edit | blame | history | raw

法律条文查询API对接数据展示

变更概述

将法律条文查询功能从Mock数据迁移到真实API对接,实现完整的数据加载、查询过滤、分页展示和详情查看流程。

背景与动机

当前LawSearchContent组件使用静态Mock数据,无法满足实际业务需求。需要对接后端API实现:
1. 页面加载时自动获取热门法律法规
2. 动态加载分类统计数据用于筛选器
3. 支持多条件组合查询和分页
4. 点击展开时动态加载法条内容
5. 弹出详情窗口展示完整法律原文

核心目标

  1. 默认数据加载:页面打开时调用getHotLaws获取热门法律列表
  2. 分类统计加载:调用getCategoryStatistics获取筛选器数据(法律性质、制定机关、时效性)
  3. 查询功能:支持关键词、日期范围、多维度筛选的组合查询
  4. 展开加载法条:点击卡片展开时调用getLawProvisions获取条文内容
  5. 详情弹窗:点击展开卡片时弹出详情窗,调用getLawOriginalDetail获取原文并解析章节导航

设计决策

1. API调用时机

场景 API 触发时机 参数
页面初始化 getHotLaws useEffect首次加载 limit=10
页面初始化 getCategoryStatistics useEffect首次加载
点击查询按钮 searchLaws 用户触发 keyword, publishStart, publishEnd, lawNatures, authorities, validities, page, size
点击卡片展开 getLawProvisions 首次展开时 law_info_id
点击展开卡片 getLawOriginalDetail 弹窗打开时 law_original_info_id

2. 数据映射

列表数据映射(从API返回到UI展示)

// API返回: lawDataList.data.data
{
  law_info_id: "xxx",              // 法律信息ID(用于getLawProvisions)
  law_original_info_id: "yyy",     // 法律原文ID(用于getLawOriginalDetail)
  title: "中华人民共和国劳动法",    // → 法律标题
  validity_name: "有效",            // → 时效性
  law_nature_name: "法律",          // → 法律效力位阶
  authority_name: "全国人民代表大会", // → 制定机关
  publish_time: "2020-05-28",      // → 公布日期
  implementation_time: "2021-01-01" // → 实施日期
}

分类统计数据映射

// API返回: categoryResponse.data
[
  { code: 101, name: "全国人民代表大会", value: "1", count: 256 },
  { code: 102, name: "法律", value: "1", count: 300 },
  { code: 103, name: "有效", value: "1", count: 500 }
]

// 分组逻辑:
// - code=101 → 制定机关筛选器
// - code=102 → 法律性质筛选器
// - code=103 → 时效性筛选器
// 每组按count降序排列,默认勾选第一项

3. 查询参数构建

// 筛选器选中项转换为API参数
const buildSearchParams = () => {
  const params = {
    page: currentPage,
    size: pageSize
  };
  
  // 关键词(非空才传)
  if (keyword.trim()) {
    params.keyword = keyword.trim();
  }
  
  // 公布日期(两个都有值才传)
  if (publishStart && publishEnd) {
    params.publishStart = publishStart;  // YYYY-MM-DD格式
    params.publishEnd = publishEnd;
  }
  
  // 法律性质(多个用逗号拼接)
  const selectedNatures = filters.lawNature
    .filter(item => item.checked)
    .map(item => item.value)
    .join(',');
  if (selectedNatures) {
    params.lawNatures = selectedNatures;
  }
  
  // 制定机关(多个用逗号拼接)
  const selectedAuthorities = filters.org
    .filter(item => item.checked)
    .map(item => item.value)
    .join(',');
  if (selectedAuthorities) {
    params.authorities = selectedAuthorities;
  }
  
  // 时效性(多个用逗号拼接)
  const selectedValidities = filters.validity
    .filter(item => item.checked)
    .map(item => item.value)
    .join(',');
  if (selectedValidities) {
    params.validities = selectedValidities;
  }
  
  return params;
};

4. 章节导航提取逻辑

provision_text (纯文本) 提取章节列表:

const extractChapters = (provisionText) => {
  // 1. 定位目录区域
  const startMarker = "\n\n目  录\n";
  const endMarker = "\n\n第一章 总则\n\n";
  
  const startIndex = provisionText.indexOf(startMarker);
  const endIndex = provisionText.indexOf(endMarker);
  
  if (startIndex === -1 || endIndex === -1) {
    return []; // 未找到目录标记,返回空数组
  }
  
  // 2. 提取目录内容
  const tocContent = provisionText.substring(
    startIndex + startMarker.length, 
    endIndex
  );
  
  // 3. 按行分割并过滤空行
  const lines = tocContent.split('\n').filter(line => line.trim());
  
  // 4. 转换为章节对象
  return lines.map((line, index) => ({
    id: `chapter-${index + 1}`,
    title: line.trim()
  }));
};

5. 状态管理策略

const [loading, setLoading] = useState(false);        // 加载中状态
const [list, setList] = useState([]);                 // 法律列表
const [filters, setFilters] = useState({              // 筛选器数据
  lawNature: [],   // 从API加载
  org: [],         // 从API加载
  validity: []     // 从API加载
});
const [activeId, setActiveId] = useState(null);       // 当前展开的卡片ID
const [expandedProvisions, setExpandedProvisions] = useState({}); // 已加载的法条内容 {law_info_id: articles}
const [currentPage, setCurrentPage] = useState(1);    // 当前页码
const [total, setTotal] = useState(0);                // 总记录数
const [pageSize, setPageSize] = useState(10);         // 每页数量

6. 错误处理与降级

  • API调用失败时显示Toast提示,不降级到Mock数据
  • 筛选器加载失败时禁用查询按钮
  • 法条内容加载失败时展示"加载失败,请重试"
  • 详情弹窗加载失败时显示错误提示

技术实现要点

涉及文件

  1. web-app/src/components/tools/LawSearchContent.jsx - 主要修改
  2. web-app/src/services/LawAPIService.js - 确认API方法签名
  3. web-app/src/components/tools/LawDetailContent.jsx - 详情组件修改

关键修改点

1. 初始化数据加载

useEffect(() => {
  const loadInitialData = async () => {
    setLoading(true);
    try {
      const [hotLawsRes, categoryRes] = await Promise.all([
        LawAPIService.getHotLaws(10),
        LawAPIService.getCategoryStatistics()
      ]);
      
      // 处理法律列表
      setList(hotLawsRes.data?.data || []);
      setTotal(hotLawsRes.data?.total || 0);
      setPageSize(hotLawsRes.data?.size || 10);
      
      // 处理分类统计
      const categories = categoryRes.data || [];
      setFilters({
        lawNature: processCategory(categories, 102),
        org: processCategory(categories, 101),
        validity: processCategory(categories, 103)
      });
    } catch (error) {
      message.error('数据加载失败,请刷新重试');
    } finally {
      setLoading(false);
    }
  };
  
  loadInitialData();
}, []);

2. 查询功能

const handleSearch = async () => {
  setLoading(true);
  try {
    const params = buildSearchParams();
    const response = await LawAPIService.searchLaws(params);
    
    setList(response.data?.data || []);
    setTotal(response.data?.total || 0);
    setPageSize(response.data?.size || 10);
    setCurrentPage(params.page);
    
    // 清空已展开的法条缓存
    setExpandedProvisions({});
    setActiveId(null);
  } catch (error) {
    message.error('查询失败,请重试');
  } finally {
    setLoading(false);
  }
};

3. 展开加载法条

const handleLawItemClick = async (law) => {
  const lawInfoId = law.law_info_id;
  
  if (activeId === lawInfoId) {
    // 已展开状态,弹出详情
    setSelectedLawId(law.law_original_info_id);
    setDetailVisible(true);
  } else {
    // 首次展开,加载法条内容
    setActiveId(lawInfoId);
    
    if (!expandedProvisions[lawInfoId]) {
      try {
        const response = await LawAPIService.getLawProvisions(lawInfoId);
        setExpandedProvisions(prev => ({
          ...prev,
          [lawInfoId]: response.data || []
        }));
      } catch (error) {
        message.error('法条内容加载失败');
      }
    }
  }
};

4. 详情弹窗

// LawDetailContent.jsx修改
useEffect(() => {
  const loadDetail = async () => {
    if (!lawId) return;
    
    setLoading(true);
    try {
      const response = await LawAPIService.getLawOriginalDetail(lawId);
      const detailData = response.data;
      
      // 从provision_text提取章节
      const chapters = extractChapters(detailData.provision_text || '');
      
      setLawDetail({
        ...detailData,
        chapters
      });
    } catch (error) {
      message.error('详情加载失败');
    } finally {
      setLoading(false);
    }
  };
  
  loadDetail();
}, [lawId]);

验收标准

功能验收

  • [ ] 页面打开时自动显示热门法律列表(10条)
  • [ ] 筛选器显示从API加载的分类数据,默认勾选第一项
  • [ ] 点击查询按钮能正确传递所有筛选条件
  • [ ] 分页组件根据API返回的total和size正确显示
  • [ ] 点击卡片首次展开时加载法条内容
  • [ ] 点击已展开卡片能弹出详情窗口
  • [ ] 详情窗口显示完整法律信息和章节导航
  • [ ] 章节导航点击能锚点跳转到对应内容

数据验收

  • [ ] 列表展示的字段与API返回字段正确映射
  • [ ] 筛选器按code分组且按count降序排列
  • [ ] 查询参数正确处理空值(空值不传)
  • [ ] 多选筛选器的value用逗号拼接
  • [ ] 分页切换时保持查询条件

异常处理

  • [ ] API调用失败时显示友好提示
  • [ ] 加载中状态正确显示Spin遮罩
  • [ ] 无数据时显示"暂无数据"提示
  • [ ] 网络错误能捕获并提示用户

风险与注意事项

  1. API返回数据结构:需要与后端确认实际返回的数据结构是否与文档一致
  2. 章节提取逻辑:provision_text格式可能存在变化,需要容错处理
  3. 性能考虑:展开法条时的API调用需要加载状态反馈
  4. 缓存策略:已展开的法条内容应缓存,避免重复请求
  5. 分页保持:切换页码时需要保持当前的查询条件和筛选状态

参考资料

  • 原型文件:document/原型/law_search.html
  • 详情原型:document/原型/law_search_detail.html
  • API Service:web-app/src/services/LawAPIService.js
  • 现有组件:web-app/src/components/tools/LawSearchContent.jsx