feat: add legal articles admin page
This commit is contained in:
@@ -5,6 +5,9 @@ const adminRouteMap: Record<string, string> = {
|
||||
files: "/files",
|
||||
knowledge: "/knowledge-bases",
|
||||
"knowledge-bases": "/knowledge-bases",
|
||||
"legal-articles": "/legal-articles",
|
||||
"legal-fulltext": "/legal-articles",
|
||||
laws: "/legal-articles",
|
||||
governance: "/governance",
|
||||
rules: "/rules",
|
||||
templates: "/templates",
|
||||
|
||||
@@ -19,6 +19,12 @@ const adminSections = [
|
||||
label: "知识库管理",
|
||||
description: "维护知识文档、索引状态和问答依据。",
|
||||
},
|
||||
{
|
||||
href: "/legal-articles",
|
||||
eyebrow: "Legal Fulltext",
|
||||
label: "法条全文库",
|
||||
description: "查看已吸收入库的法律法规全文、索引状态和法源追溯。",
|
||||
},
|
||||
{
|
||||
href: "/rules",
|
||||
eyebrow: "Rules",
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
"use client";
|
||||
|
||||
import { EyeOutlined, FileSearchOutlined, LinkOutlined, ReloadOutlined, SearchOutlined } from "@ant-design/icons";
|
||||
import { Alert, Button, Card, Drawer, Empty, Form, Input, Select, Space, Statistic, Table, Tag, Typography, message } from "antd";
|
||||
import type { ColumnsType, TablePaginationConfig } from "antd/es/table";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { getLegalArticle, listLegalArticles } from "@/services/legal-article";
|
||||
import { useAuthStore } from "@/stores/auth.store";
|
||||
import type { LegalArticleItem } from "@/types/api";
|
||||
|
||||
const { Paragraph, Text, Title } = Typography;
|
||||
|
||||
type LegalArticleFilterValues = {
|
||||
keyword?: string;
|
||||
source_id?: string;
|
||||
article_no?: string;
|
||||
index_status?: string;
|
||||
published_status?: string;
|
||||
};
|
||||
|
||||
const defaultFilters: LegalArticleFilterValues = {
|
||||
keyword: "",
|
||||
source_id: "",
|
||||
article_no: "",
|
||||
index_status: "all",
|
||||
published_status: "all",
|
||||
};
|
||||
|
||||
const indexStatusOptions = [
|
||||
{ value: "all", label: "全部索引状态" },
|
||||
{ value: "indexed", label: "已索引" },
|
||||
{ value: "pending", label: "待索引" },
|
||||
{ value: "processing", label: "索引中" },
|
||||
{ value: "failed", label: "索引失败" },
|
||||
];
|
||||
|
||||
const publishedStatusOptions = [
|
||||
{ value: "all", label: "全部发布状态" },
|
||||
{ value: "published", label: "已发布" },
|
||||
{ value: "draft", label: "草稿" },
|
||||
{ value: "archived", label: "已归档" },
|
||||
];
|
||||
|
||||
const indexStatusMeta: Record<string, { color: string; label: string }> = {
|
||||
indexed: { color: "green", label: "已索引" },
|
||||
pending: { color: "gold", label: "待索引" },
|
||||
processing: { color: "blue", label: "索引中" },
|
||||
failed: { color: "red", label: "索引失败" },
|
||||
};
|
||||
|
||||
const publishedStatusMeta: Record<string, { color: string; label: string }> = {
|
||||
published: { color: "green", label: "已发布" },
|
||||
draft: { color: "default", label: "草稿" },
|
||||
archived: { color: "purple", label: "已归档" },
|
||||
};
|
||||
|
||||
function formatDateTime(value: string | null | undefined) {
|
||||
if (!value) {
|
||||
return "-";
|
||||
}
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value;
|
||||
}
|
||||
return date.toLocaleString("zh-CN", { hour12: false });
|
||||
}
|
||||
|
||||
function formatDate(value: string | null | undefined) {
|
||||
if (!value) {
|
||||
return "-";
|
||||
}
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value;
|
||||
}
|
||||
return date.toLocaleDateString("zh-CN");
|
||||
}
|
||||
|
||||
function formatHash(value: string | null | undefined) {
|
||||
return value ? `${value.slice(0, 12)}...` : "-";
|
||||
}
|
||||
|
||||
function renderStatusTag(value: string, metaMap: Record<string, { color: string; label: string }>) {
|
||||
const meta = metaMap[value] ?? { color: "default", label: value || "未知" };
|
||||
return <Tag color={meta.color}>{meta.label}</Tag>;
|
||||
}
|
||||
|
||||
function normalizeFilters(values: LegalArticleFilterValues): LegalArticleFilterValues {
|
||||
return {
|
||||
keyword: values.keyword?.trim() ?? "",
|
||||
source_id: values.source_id?.trim() ?? "",
|
||||
article_no: values.article_no?.trim() ?? "",
|
||||
index_status: values.index_status ?? "all",
|
||||
published_status: values.published_status ?? "all",
|
||||
};
|
||||
}
|
||||
|
||||
export default function LegalArticlesPage() {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const currentUser = useAuthStore((state) => state.currentUser);
|
||||
const [form] = Form.useForm<LegalArticleFilterValues>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [items, setItems] = useState<LegalArticleItem[]>([]);
|
||||
const [filters, setFilters] = useState<LegalArticleFilterValues>(defaultFilters);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [selectedArticle, setSelectedArticle] = useState<LegalArticleItem | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
const currentPageSourceCount = useMemo(() => new Set(items.map((item) => item.source_id)).size, [items]);
|
||||
const currentPageIndexedCount = useMemo(() => items.filter((item) => item.index_status === "indexed").length, [items]);
|
||||
const currentPagePublishedCount = useMemo(() => items.filter((item) => item.published_status === "published").length, [items]);
|
||||
|
||||
const loadArticles = useCallback(
|
||||
async (nextPage = page, nextPageSize = pageSize, nextFilters = filters) => {
|
||||
if (currentUser?.role !== "admin") {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await listLegalArticles({
|
||||
page: nextPage,
|
||||
page_size: nextPageSize,
|
||||
...nextFilters,
|
||||
});
|
||||
if (result.code !== 0) {
|
||||
messageApi.error(result.message || "法条全文库加载失败");
|
||||
return;
|
||||
}
|
||||
setItems(result.data.items);
|
||||
setPage(result.data.page);
|
||||
setPageSize(result.data.page_size);
|
||||
setTotal(result.data.total);
|
||||
} catch (error) {
|
||||
messageApi.error(error instanceof Error ? error.message : "法条全文库加载失败,请稍后重试");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[currentUser?.role, filters, messageApi, page, pageSize],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => {
|
||||
void loadArticles();
|
||||
}, 0);
|
||||
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [loadArticles]);
|
||||
|
||||
const handleSearch = (values: LegalArticleFilterValues) => {
|
||||
const nextFilters = normalizeFilters(values);
|
||||
setFilters(nextFilters);
|
||||
setLoading(true);
|
||||
void loadArticles(1, pageSize, nextFilters);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
form.setFieldsValue(defaultFilters);
|
||||
setFilters(defaultFilters);
|
||||
setLoading(true);
|
||||
void loadArticles(1, pageSize, defaultFilters);
|
||||
};
|
||||
|
||||
const handleTableChange = (pagination: TablePaginationConfig) => {
|
||||
const nextPage = pagination.current ?? 1;
|
||||
const nextPageSize = pagination.pageSize ?? pageSize;
|
||||
setLoading(true);
|
||||
void loadArticles(nextPage, nextPageSize, filters);
|
||||
};
|
||||
|
||||
const handleOpenDetail = async (item: LegalArticleItem) => {
|
||||
setSelectedArticle(item);
|
||||
setDrawerOpen(true);
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
const result = await getLegalArticle(item.id);
|
||||
if (result.code !== 0) {
|
||||
messageApi.error(result.message || "读取法条详情失败");
|
||||
return;
|
||||
}
|
||||
setSelectedArticle(result.data);
|
||||
} catch (error) {
|
||||
messageApi.error(error instanceof Error ? error.message : "读取法条详情失败,请稍后重试");
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<LegalArticleItem> = [
|
||||
{
|
||||
title: "法源",
|
||||
dataIndex: "source_title",
|
||||
key: "source_title",
|
||||
width: 260,
|
||||
render: (_, item) => (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Text strong>{item.source_title}</Text>
|
||||
<Text code>{item.source_id}</Text>
|
||||
<Text type="secondary">{item.authority}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "条文编号",
|
||||
dataIndex: "article_no",
|
||||
key: "article_no",
|
||||
width: 120,
|
||||
render: (value: string) => <Text strong>{value}</Text>,
|
||||
},
|
||||
{
|
||||
title: "章节",
|
||||
key: "chapter",
|
||||
width: 220,
|
||||
render: (_, item) => (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Text>{item.chapter_title || "-"}</Text>
|
||||
<Text type="secondary">{item.section_title || "-"}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "法条摘要",
|
||||
dataIndex: "article_text",
|
||||
key: "article_text",
|
||||
width: 360,
|
||||
render: (value: string) => (
|
||||
<Paragraph ellipsis={{ rows: 2 }} style={{ marginBottom: 0 }}>
|
||||
{value}
|
||||
</Paragraph>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "标签",
|
||||
dataIndex: "tags",
|
||||
key: "tags",
|
||||
width: 180,
|
||||
render: (value: string[]) => (
|
||||
<Space wrap size={4}>
|
||||
{value?.length ? value.slice(0, 3).map((tag) => <Tag key={tag}>{tag}</Tag>) : <Text type="secondary">-</Text>}
|
||||
{value?.length > 3 ? <Tag>+{value.length - 3}</Tag> : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "知识库吸收",
|
||||
key: "knowledge",
|
||||
width: 220,
|
||||
render: (_, item) => (
|
||||
<Space direction="vertical" size={2}>
|
||||
{renderStatusTag(item.index_status, indexStatusMeta)}
|
||||
<Text type="secondary">知识库:{item.knowledge_base_id || "-"}</Text>
|
||||
<Text type="secondary">文档:{item.knowledge_document_id || "-"}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "发布状态",
|
||||
key: "published_status",
|
||||
width: 160,
|
||||
render: (_, item) => (
|
||||
<Space direction="vertical" size={2}>
|
||||
{renderStatusTag(item.published_status, publishedStatusMeta)}
|
||||
<Text type="secondary">{formatDateTime(item.published_at)}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "生效范围",
|
||||
key: "effective",
|
||||
width: 160,
|
||||
render: (_, item) => (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Text>{item.region}</Text>
|
||||
<Text type="secondary">{formatDate(item.effective_date)}</Text>
|
||||
<Text type="secondary">{item.validity_status || "-"}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
fixed: "right",
|
||||
width: 120,
|
||||
render: (_, item) => (
|
||||
<Button icon={<EyeOutlined />} size="small" onClick={() => void handleOpenDetail(item)}>
|
||||
查看全文
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (currentUser?.role !== "admin") {
|
||||
return (
|
||||
<main className="page-shell review-history-page">
|
||||
{contextHolder}
|
||||
<Card className="review-list-card">
|
||||
<Alert message="仅管理员可查看法条全文库。" type="warning" showIcon />
|
||||
</Card>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="page-shell review-page-grid">
|
||||
{contextHolder}
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<section className="review-page-hero">
|
||||
<div className="review-page-hero-copy">
|
||||
<span className="landing-badge">Legal Fulltext Library</span>
|
||||
<Title level={2}>法条全文库</Title>
|
||||
<Text type="secondary">查看 Phase14 已吸收入库的法律法规全文条款,核对法源、索引状态、发布状态和知识库追溯信息。</Text>
|
||||
</div>
|
||||
<Card className="review-side-card" bordered={false}>
|
||||
<span className="landing-panel-eyebrow">Phase14</span>
|
||||
<Title level={4} style={{ margin: 0, color: "inherit" }}>
|
||||
吸收闭环
|
||||
</Title>
|
||||
<ul className="review-page-tip-list">
|
||||
<li>全文条款由治理资产资料包同步到业务运行层。</li>
|
||||
<li>本页只查看和核验,不替代正式法源导入脚本。</li>
|
||||
<li>每条法条保留知识库、文档、校验值和发布时间。</li>
|
||||
</ul>
|
||||
<Link href="/knowledge-bases">
|
||||
<Button icon={<FileSearchOutlined />}>查看知识库</Button>
|
||||
</Link>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<Space size="middle" wrap>
|
||||
<Card className="review-surface-card" style={{ minWidth: 180 }}>
|
||||
<Statistic title="法条总数" value={total} />
|
||||
</Card>
|
||||
<Card className="review-surface-card" style={{ minWidth: 180 }}>
|
||||
<Statistic title="当前页法源" value={currentPageSourceCount} />
|
||||
</Card>
|
||||
<Card className="review-surface-card" style={{ minWidth: 180 }}>
|
||||
<Statistic title="当前页已索引" value={currentPageIndexedCount} suffix={`/ ${items.length}`} />
|
||||
</Card>
|
||||
<Card className="review-surface-card" style={{ minWidth: 180 }}>
|
||||
<Statistic title="当前页已发布" value={currentPagePublishedCount} suffix={`/ ${items.length}`} />
|
||||
</Card>
|
||||
</Space>
|
||||
|
||||
<Card className="review-surface-card" title="检索条件">
|
||||
<Form form={form} layout="vertical" initialValues={defaultFilters} onFinish={handleSearch}>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", gap: 16 }}>
|
||||
<Form.Item label="关键词" name="keyword">
|
||||
<Input allowClear placeholder="搜索法源、条号或全文" prefix={<SearchOutlined />} />
|
||||
</Form.Item>
|
||||
<Form.Item label="法源编号" name="source_id">
|
||||
<Input allowClear placeholder="例如 LAW-CIVIL-CODE-2020" />
|
||||
</Form.Item>
|
||||
<Form.Item label="条文编号" name="article_no">
|
||||
<Input allowClear placeholder="例如 第四百六十四条" />
|
||||
</Form.Item>
|
||||
<Form.Item label="索引状态" name="index_status">
|
||||
<Select options={indexStatusOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item label="发布状态" name="published_status">
|
||||
<Select options={publishedStatusOptions} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit" icon={<SearchOutlined />}>
|
||||
查询
|
||||
</Button>
|
||||
<Button onClick={handleReset} icon={<ReloadOutlined />}>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Card className="review-surface-card" title="法条列表">
|
||||
<Table
|
||||
rowKey="id"
|
||||
dataSource={items}
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
pagination={{ current: page, pageSize, total, showSizeChanger: true }}
|
||||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="还没有法条全文数据。" /> }}
|
||||
onChange={handleTableChange}
|
||||
scroll={{ x: 1720 }}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
|
||||
<Drawer
|
||||
title={selectedArticle ? `${selectedArticle.source_title} ${selectedArticle.article_no}` : "法条全文"}
|
||||
open={drawerOpen}
|
||||
width={760}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
>
|
||||
<Card loading={detailLoading} bordered={false}>
|
||||
{selectedArticle ? (
|
||||
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
|
||||
<Space wrap>
|
||||
{renderStatusTag(selectedArticle.index_status, indexStatusMeta)}
|
||||
{renderStatusTag(selectedArticle.published_status, publishedStatusMeta)}
|
||||
<Tag>{selectedArticle.source_type}</Tag>
|
||||
<Tag>{selectedArticle.region}</Tag>
|
||||
</Space>
|
||||
<div>
|
||||
<Text type="secondary">法源</Text>
|
||||
<Title level={4} style={{ marginTop: 4 }}>
|
||||
{selectedArticle.source_title}
|
||||
</Title>
|
||||
<Text>{selectedArticle.authority}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary">章节</Text>
|
||||
<Paragraph style={{ marginTop: 4, marginBottom: 0 }}>
|
||||
{[selectedArticle.chapter_title, selectedArticle.section_title].filter(Boolean).join(" / ") || "-"}
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary">全文</Text>
|
||||
<Paragraph copyable={{ text: selectedArticle.article_text }} style={{ marginTop: 8, whiteSpace: "pre-wrap" }}>
|
||||
{selectedArticle.article_text}
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Space wrap>
|
||||
{selectedArticle.tags.length ? selectedArticle.tags.map((tag) => <Tag key={tag}>{tag}</Tag>) : <Tag>暂无标签</Tag>}
|
||||
</Space>
|
||||
<Card size="small" title="追溯信息">
|
||||
<Space direction="vertical" size={6} style={{ width: "100%" }}>
|
||||
<Text>法源编号:{selectedArticle.source_id}</Text>
|
||||
<Text>生效日期:{formatDate(selectedArticle.effective_date)}</Text>
|
||||
<Text>有效性:{selectedArticle.validity_status || "-"}</Text>
|
||||
<Text>知识库:{selectedArticle.knowledge_base_id || "-"}</Text>
|
||||
<Text>知识文档:{selectedArticle.knowledge_document_id || "-"}</Text>
|
||||
<Text>全文校验值:{formatHash(selectedArticle.text_checksum)}</Text>
|
||||
<Text>法源校验值:{formatHash(selectedArticle.source_checksum)}</Text>
|
||||
<Text>更新时间:{formatDateTime(selectedArticle.updated_at)}</Text>
|
||||
{selectedArticle.official_url ? (
|
||||
<a href={selectedArticle.official_url} target="_blank" rel="noreferrer">
|
||||
<LinkOutlined /> 打开官方来源
|
||||
</a>
|
||||
) : null}
|
||||
</Space>
|
||||
</Card>
|
||||
</Space>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="请选择一条法条。" />
|
||||
)}
|
||||
</Card>
|
||||
</Drawer>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
DeploymentUnitOutlined,
|
||||
DownOutlined,
|
||||
FileDoneOutlined,
|
||||
FileSearchOutlined,
|
||||
FileProtectOutlined,
|
||||
FolderOpenOutlined,
|
||||
HistoryOutlined,
|
||||
@@ -76,6 +77,7 @@ const adminNavItems = [
|
||||
{ href: "/rules", label: "规则管理", icon: <SolutionOutlined /> },
|
||||
{ href: "/templates", label: "模板管理", icon: <BookOutlined /> },
|
||||
{ href: "/knowledge-bases", label: "知识库管理", icon: <FolderOpenOutlined /> },
|
||||
{ href: "/legal-articles", label: "法条全文库", icon: <FileSearchOutlined /> },
|
||||
{ href: "/files", label: "文件管理", icon: <FileDoneOutlined /> },
|
||||
{ href: "/tenant-settings", label: "租户设置", icon: <SettingOutlined /> },
|
||||
{ href: "/users", label: "用户管理", icon: <TeamOutlined /> },
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { http } from "@/services/http";
|
||||
import type { ApiResponse, LegalArticleItem, LegalArticleListParams, LegalArticleListResponse } from "@/types/api";
|
||||
|
||||
function cleanParams(params: LegalArticleListParams) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(params).filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== "" && value !== "all"),
|
||||
);
|
||||
}
|
||||
|
||||
export async function listLegalArticles(params: LegalArticleListParams = {}) {
|
||||
const response = await http.get<ApiResponse<LegalArticleListResponse>>("/legal-articles", {
|
||||
params: cleanParams(params),
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getLegalArticle(articleId: string) {
|
||||
const response = await http.get<ApiResponse<LegalArticleItem>>(`/legal-articles/${articleId}`);
|
||||
return response.data;
|
||||
}
|
||||
@@ -299,6 +299,47 @@ export type KnowledgeBaseRollbackPayload = {
|
||||
export type KnowledgeBaseListResponse = PageResponse<KnowledgeBaseItem>;
|
||||
export type KnowledgeBaseDocumentListResponse = PageResponse<KnowledgeBaseDocumentItem>;
|
||||
|
||||
export type LegalArticleItem = {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
source_id: string;
|
||||
source_title: string;
|
||||
source_type: string;
|
||||
authority: string;
|
||||
effective_date: string | null;
|
||||
validity_status: string | null;
|
||||
region: string;
|
||||
official_url: string | null;
|
||||
article_no: string;
|
||||
chapter_title: string | null;
|
||||
section_title: string | null;
|
||||
article_text: string;
|
||||
tags: string[];
|
||||
tags_json: string[] | null;
|
||||
source_checksum: string | null;
|
||||
text_checksum: string | null;
|
||||
knowledge_base_id: string | null;
|
||||
knowledge_document_id: string | null;
|
||||
index_status: string;
|
||||
published_status: string;
|
||||
published_at: string | null;
|
||||
published_by: string | null;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
};
|
||||
|
||||
export type LegalArticleListParams = {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
keyword?: string;
|
||||
source_id?: string;
|
||||
article_no?: string;
|
||||
index_status?: string;
|
||||
published_status?: string;
|
||||
};
|
||||
|
||||
export type LegalArticleListResponse = PageResponse<LegalArticleItem>;
|
||||
|
||||
export type ReviewRuleItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -2093,10 +2093,53 @@
|
||||
- Phase14 目前完成的是首批 6 个法源、33 条高频合同相关法条,不是全国全部法律法规全文。
|
||||
- 后续增强项:
|
||||
- 扩展更多民法典合同编和相关司法解释条文
|
||||
- 增加前端“法条全文库”管理页面
|
||||
- 在审查报告中把风险依据进一步链接到 `legal_articles` 的具体条文全文
|
||||
- 支持用户本地下载的法律法规资料包按同一清单格式导入
|
||||
|
||||
### 阻塞项
|
||||
- 本轮无 required 阻塞项。
|
||||
- 仍保留旧的非 required 项:外部告警 Webhook 未配置。
|
||||
|
||||
## 2026-05-16 Phase14 法条全文库前端入口闭环
|
||||
### 本轮完成项
|
||||
- 已新增管理员前端页面:
|
||||
- `frontend/src/app/legal-articles/page.tsx`
|
||||
- 路由:`/legal-articles`
|
||||
- 能查看法条总数、当前页法源数、索引状态、发布状态、知识库编号、知识文档编号、法源元数据和全文抽屉。
|
||||
- 已新增前端服务和类型:
|
||||
- `frontend/src/services/legal-article.ts`
|
||||
- `frontend/src/types/api.ts`
|
||||
- 已接入后台入口:
|
||||
- 管理顶部菜单新增“法条全文库”
|
||||
- 后台首页新增“法条全文库”卡片
|
||||
- `/admin/legal-articles`、`/admin/legal-fulltext`、`/admin/laws` 会跳转到 `/legal-articles`
|
||||
|
||||
### 本轮验证
|
||||
- `npm --prefix frontend run lint`
|
||||
- 结果:通过
|
||||
- `npm --prefix frontend run build`
|
||||
- 结果:通过
|
||||
- 构建路由包含:`/legal-articles`
|
||||
- `git diff --check`
|
||||
- 结果:通过,仅有 Git CRLF 工作区提示
|
||||
- Playwright 前端冒烟:
|
||||
- 地址:`http://127.0.0.1:3000/legal-articles`
|
||||
- 方式:模拟管理员会话并拦截 `/api/v1/legal-articles` 接口
|
||||
- 结果:`PASS`
|
||||
- 已验证:页面标题、总数、法条列表和“查看全文”抽屉。
|
||||
|
||||
### 当前状态
|
||||
- Phase14 现在不只是后端和导入脚本可用,管理员也能在 Web 后台直接查看“法条全文库”。
|
||||
- 当前前端定位是查询、核验和追溯,不做人工录入法条,避免正式法源维护退回人工填表。
|
||||
- 本地前端 dev server 已启动:`http://127.0.0.1:3000/legal-articles`
|
||||
|
||||
### 未完成项
|
||||
- Phase14 首批仍是 6 个法源、33 条高频合同相关条文,不是全国全部法律法规全文。
|
||||
- 后续增强项:
|
||||
- 扩展更多民法典合同编和相关司法解释条文
|
||||
- 在审查报告中把风险依据链接到 `legal_articles` 具体条文全文
|
||||
- 支持用户本地下载的法律法规资料包按同一清单格式导入
|
||||
|
||||
### 阻塞项
|
||||
- 本轮无 required 阻塞项。
|
||||
- 仍保留旧的非 required 项:外部告警 Webhook 未配置。
|
||||
|
||||
Reference in New Issue
Block a user