refactor: align document type page with root groups
This commit is contained in:
@@ -1032,11 +1032,15 @@ function buildFallbackRoutes(roleKey: string): {
|
||||
};
|
||||
}
|
||||
|
||||
function isLegacyRuleSetsMenu(path: string | undefined): boolean {
|
||||
return path === '/rules/sets';
|
||||
}
|
||||
|
||||
function normalizeMenuStructure(menuItems: MenuItem[]): MenuItem[] {
|
||||
const clonedMenuItems = menuItems.map(item => ({
|
||||
...item,
|
||||
children: item.children ? normalizeMenuStructure(item.children) : undefined,
|
||||
}));
|
||||
})).filter(item => !isLegacyRuleSetsMenu(item.path));
|
||||
|
||||
const collectDescendantPaths = (items: MenuItem[] | undefined): string[] => {
|
||||
if (!items || items.length === 0) {
|
||||
|
||||
@@ -16,6 +16,36 @@ export interface DocumentType {
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface DocumentTypeRoot {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
description: string | null;
|
||||
entryModuleId: number | null;
|
||||
entryModuleName?: string | null;
|
||||
isEnabled: boolean;
|
||||
childGroupCount: number;
|
||||
ruleSetCount: number;
|
||||
ruleSetIds: number[];
|
||||
}
|
||||
|
||||
export interface DocumentTypeRootCreateDTO {
|
||||
code: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
entryModuleId?: number | null;
|
||||
isEnabled?: boolean;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface DocumentTypeRootUpdateDTO {
|
||||
name?: string;
|
||||
description?: string;
|
||||
entryModuleId?: number | null;
|
||||
isEnabled?: boolean;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface DocumentTypeCreateDTO {
|
||||
code: string;
|
||||
name: string;
|
||||
@@ -273,3 +303,123 @@ export async function getRuleSets(
|
||||
return { error: error instanceof Error ? error.message : "获取规则集失败" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDocumentTypeRoots(
|
||||
searchParams: { entry_module_id?: number } = {},
|
||||
token?: string,
|
||||
): Promise<{ data?: { types: DocumentTypeRoot[]; total: number }; error?: string; status?: number }> {
|
||||
try {
|
||||
const params: Record<string, number> = {};
|
||||
if (searchParams.entry_module_id) params.entry_module_id = searchParams.entry_module_id;
|
||||
const response = await axios.get(`${API_BASE_URL}/api/v3/document-type-roots`, {
|
||||
params,
|
||||
headers: authHeaders(token),
|
||||
});
|
||||
const items = extractData<any[]>(response) || [];
|
||||
const types: DocumentTypeRoot[] = items.map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
code: item.code,
|
||||
description: item.description || null,
|
||||
entryModuleId: item.entryModuleId || null,
|
||||
entryModuleName: item.entryModuleName || null,
|
||||
isEnabled: item.isEnabled !== false,
|
||||
childGroupCount: Number(item.childGroupCount || 0),
|
||||
ruleSetCount: Number(item.ruleSetCount || 0),
|
||||
ruleSetIds: item.ruleSetIds || [],
|
||||
}));
|
||||
return { data: { types, total: types.length } };
|
||||
} catch (error) {
|
||||
return { error: error instanceof Error ? error.message : "获取一级文档类型失败", status: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDocumentTypeRoot(
|
||||
id: number,
|
||||
token?: string,
|
||||
): Promise<{ data?: DocumentTypeRoot; error?: string; status?: number }> {
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/api/v3/document-type-roots/${id}`, {
|
||||
headers: authHeaders(token),
|
||||
});
|
||||
const item = extractData<any>(response);
|
||||
if (!item) return { error: "一级文档类型不存在", status: 404 };
|
||||
return {
|
||||
data: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
code: item.code,
|
||||
description: item.description || null,
|
||||
entryModuleId: item.entryModuleId || null,
|
||||
entryModuleName: item.entryModuleName || null,
|
||||
isEnabled: item.isEnabled !== false,
|
||||
childGroupCount: Number(item.childGroupCount || 0),
|
||||
ruleSetCount: Number(item.ruleSetCount || 0),
|
||||
ruleSetIds: item.ruleSetIds || [],
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return { error: error instanceof Error ? error.message : "获取一级文档类型失败", status: 500 };
|
||||
}
|
||||
}
|
||||
|
||||
export async function createDocumentTypeRoot(
|
||||
dto: DocumentTypeRootCreateDTO,
|
||||
token?: string,
|
||||
): Promise<{ data?: DocumentTypeRoot; error?: string; status?: number }> {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/api/v3/document-type-roots`, dto, {
|
||||
headers: { ...authHeaders(token), "Content-Type": "application/json" },
|
||||
});
|
||||
const item = extractData<any>(response);
|
||||
if (!item) return { error: "创建失败", status: 500 };
|
||||
return {
|
||||
data: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
code: item.code,
|
||||
description: item.description || null,
|
||||
entryModuleId: item.entryModuleId || null,
|
||||
entryModuleName: item.entryModuleName || null,
|
||||
isEnabled: item.isEnabled !== false,
|
||||
childGroupCount: Number(item.childGroupCount || 0),
|
||||
ruleSetCount: Number(item.ruleSetCount || 0),
|
||||
ruleSetIds: item.ruleSetIds || [],
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
const msg = (error as any)?.response?.data?.message || (error instanceof Error ? error.message : "创建失败");
|
||||
return { error: msg, status: (error as any)?.response?.status || 500 };
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateDocumentTypeRoot(
|
||||
id: number,
|
||||
dto: DocumentTypeRootUpdateDTO,
|
||||
token?: string,
|
||||
): Promise<{ data?: DocumentTypeRoot; error?: string; status?: number }> {
|
||||
try {
|
||||
const response = await axios.put(`${API_BASE_URL}/api/v3/document-type-roots/${id}`, dto, {
|
||||
headers: { ...authHeaders(token), "Content-Type": "application/json" },
|
||||
});
|
||||
const item = extractData<any>(response);
|
||||
if (!item) return { error: "更新失败", status: 500 };
|
||||
return {
|
||||
data: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
code: item.code,
|
||||
description: item.description || null,
|
||||
entryModuleId: item.entryModuleId || null,
|
||||
entryModuleName: item.entryModuleName || null,
|
||||
isEnabled: item.isEnabled !== false,
|
||||
childGroupCount: Number(item.childGroupCount || 0),
|
||||
ruleSetCount: Number(item.ruleSetCount || 0),
|
||||
ruleSetIds: item.ruleSetIds || [],
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
const msg = (error as any)?.response?.data?.message || (error instanceof Error ? error.message : "更新失败");
|
||||
return { error: msg, status: (error as any)?.response?.status || 500 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,7 @@ import { useNavigate, useLoaderData } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import {
|
||||
getDocumentTypes,
|
||||
deleteDocumentType,
|
||||
getEntryModules,
|
||||
type DocumentType,
|
||||
type EntryModuleOption,
|
||||
} from "~/api/document-types/document-types";
|
||||
import { getDocumentSubtypeGroupsMap, type DocumentSubtypeGroup } from "~/api/files/files-upload";
|
||||
import { getDocumentTypeRoots, type DocumentTypeRoot } from "~/api/document-types/document-types";
|
||||
import documentTypesStyles from "~/styles/pages/document-types_index.css?url";
|
||||
|
||||
export function links() {
|
||||
@@ -21,14 +13,12 @@ export function links() {
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "文档类型管理 - 中国烟草AI合同及卷宗审核系统" },
|
||||
{ name: "description", content: "管理文档类型及其汇总规则集绑定" },
|
||||
{ name: "description", content: "按一级大类管理文档类型入口归属" },
|
||||
];
|
||||
};
|
||||
|
||||
interface LoaderData {
|
||||
types: DocumentType[];
|
||||
entryModules: EntryModuleOption[];
|
||||
subtypeGroupsByTypeId: Record<number, DocumentSubtypeGroup[]>;
|
||||
rootCategories: DocumentTypeRoot[];
|
||||
frontendJWT?: string | null;
|
||||
error?: string;
|
||||
}
|
||||
@@ -37,68 +27,26 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
const [typesRes, modulesRes] = await Promise.all([
|
||||
getDocumentTypes({}, frontendJWT),
|
||||
getEntryModules(frontendJWT),
|
||||
]);
|
||||
|
||||
const types = typesRes.data?.types || [];
|
||||
const subtypeGroupsRes = await getDocumentSubtypeGroupsMap(
|
||||
types.map((type) => type.id),
|
||||
frontendJWT,
|
||||
);
|
||||
const rootsRes = await getDocumentTypeRoots({}, frontendJWT);
|
||||
|
||||
return {
|
||||
types,
|
||||
entryModules: modulesRes.data || [],
|
||||
subtypeGroupsByTypeId: "data" in subtypeGroupsRes && subtypeGroupsRes.data ? subtypeGroupsRes.data : {},
|
||||
rootCategories: rootsRes.data?.types || [],
|
||||
frontendJWT,
|
||||
};
|
||||
} catch (error) {
|
||||
return { types: [], entryModules: [], subtypeGroupsByTypeId: {}, error: "加载失败" };
|
||||
console.error("加载一级文档类型失败:", error);
|
||||
return { rootCategories: [], entryModules: [], error: "加载失败" };
|
||||
}
|
||||
}
|
||||
|
||||
export default function DocumentTypesIndex() {
|
||||
const navigate = useNavigate();
|
||||
const loaderData = useLoaderData<LoaderData>();
|
||||
const [types, setTypes] = useState<DocumentType[]>(loaderData.types || []);
|
||||
const entryModules = loaderData.entryModules || [];
|
||||
const subtypeGroupsByTypeId = loaderData.subtypeGroupsByTypeId || {};
|
||||
const [rootCategories, setRootCategories] = useState<DocumentTypeRoot[]>(loaderData.rootCategories || []);
|
||||
|
||||
useEffect(() => {
|
||||
setTypes(loaderData.types || []);
|
||||
}, [loaderData.types]);
|
||||
|
||||
const getEntryModuleName = (id: number | null) => {
|
||||
if (!id) return "—";
|
||||
return entryModules.find((m) => m.id === id)?.name || `#${id}`;
|
||||
};
|
||||
|
||||
const getSubtypeGroups = (typeId: number) => subtypeGroupsByTypeId[typeId] || [];
|
||||
|
||||
const getRootGroupSummary = (typeId: number) => {
|
||||
const groups = getSubtypeGroups(typeId);
|
||||
const rootNames = Array.from(new Set(groups.map((group) => group.rootGroupName).filter(Boolean)));
|
||||
if (rootNames.length > 0) {
|
||||
return rootNames.join("、");
|
||||
}
|
||||
return "未映射一级分组";
|
||||
};
|
||||
|
||||
const handleDelete = async (type: DocumentType) => {
|
||||
const confirmed = window.confirm(`确定要删除文档类型「${type.name}」吗?`);
|
||||
if (!confirmed) return;
|
||||
|
||||
const result = await deleteDocumentType(type.id, loaderData.frontendJWT ?? undefined);
|
||||
if (result.success) {
|
||||
toastService.success("文档类型已删除");
|
||||
setTypes((prev) => prev.filter((t) => t.id !== type.id));
|
||||
} else {
|
||||
toastService.error(result.error || "删除失败");
|
||||
}
|
||||
};
|
||||
setRootCategories(loaderData.rootCategories || []);
|
||||
}, [loaderData.rootCategories]);
|
||||
|
||||
return (
|
||||
<div className="document-types-page">
|
||||
@@ -113,10 +61,10 @@ export default function DocumentTypesIndex() {
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
{types.length === 0 ? (
|
||||
{rootCategories.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<i className="ri-inbox-line"></i>
|
||||
<p>暂无文档类型</p>
|
||||
<p>暂无一级文档类型</p>
|
||||
</div>
|
||||
) : (
|
||||
<table className="data-table">
|
||||
@@ -124,48 +72,48 @@ export default function DocumentTypesIndex() {
|
||||
<tr>
|
||||
<th>编码</th>
|
||||
<th>名称</th>
|
||||
<th>所属大类</th>
|
||||
<th>运行子类型映射</th>
|
||||
<th>入口模块</th>
|
||||
<th>一级分组</th>
|
||||
<th>二级分组</th>
|
||||
<th>汇总规则集</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{types.map((type) => (
|
||||
<tr key={type.id}>
|
||||
<td><code>{type.code}</code></td>
|
||||
{rootCategories.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<td><code>{item.code}</code></td>
|
||||
<td>
|
||||
<div className="type-cell">
|
||||
<strong>{type.name}</strong>
|
||||
<span>文档类型</span>
|
||||
<strong>{item.name}</strong>
|
||||
<span>一级文档类型</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="type-cell">
|
||||
<strong>{getEntryModuleName(type.entryModuleId)}</strong>
|
||||
<span>{getRootGroupSummary(type.id)}</span>
|
||||
<strong>{item.entryModuleName || (item.entryModuleId ? `#${item.entryModuleId}` : "—")}</strong>
|
||||
<span>入口模块</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="groups-container">
|
||||
{getSubtypeGroups(type.id).length > 0 ? (
|
||||
getSubtypeGroups(type.id).map((group) => (
|
||||
<span key={group.id} className="type-badge" title={group.displayHint || group.code}>
|
||||
{group.displayName || group.name}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="subtle-text">未映射运行子类型</span>
|
||||
)}
|
||||
<div className="type-cell">
|
||||
<strong>{item.name}</strong>
|
||||
<span>文档类型归属的大类</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span className="tag">{type.ruleSetIds?.length || 0} 个规则集</span>
|
||||
<div className="type-cell">
|
||||
<strong>{item.childGroupCount} 个</strong>
|
||||
<span>{item.childGroupCount > 0 ? "该大类下的运行子类型数量" : "尚未拆分二级分组"}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span className={`status-badge ${type.isEnabled ? "enabled" : "disabled"}`}>
|
||||
{type.isEnabled ? "启用" : "禁用"}
|
||||
<span className="tag">{item.ruleSetCount} 个规则集</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className={`status-badge ${item.isEnabled ? "enabled" : "disabled"}`}>
|
||||
{item.isEnabled ? "启用" : "禁用"}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
@@ -173,17 +121,10 @@ export default function DocumentTypesIndex() {
|
||||
<button
|
||||
className="btn-icon"
|
||||
title="编辑"
|
||||
onClick={() => navigate(`/document-types/new?id=${type.id}`)}
|
||||
onClick={() => navigate(`/document-types/new?id=${item.id}`)}
|
||||
>
|
||||
<i className="ri-edit-line"></i>
|
||||
</button>
|
||||
<button
|
||||
className="btn-icon text-error"
|
||||
title="删除"
|
||||
onClick={() => handleDelete(type)}
|
||||
>
|
||||
<i className="ri-delete-bin-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
+138
-333
@@ -1,22 +1,19 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useLoaderData } from "@remix-run/react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useLoaderData, useNavigate } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import {
|
||||
getDocumentType,
|
||||
createDocumentType,
|
||||
updateDocumentType,
|
||||
createDocumentTypeRoot,
|
||||
getDocumentTypeRoot,
|
||||
getEntryModules,
|
||||
getRuleSets,
|
||||
type DocumentType,
|
||||
type DocumentTypeCreateDTO,
|
||||
type DocumentTypeUpdateDTO,
|
||||
updateDocumentTypeRoot,
|
||||
type DocumentTypeRoot,
|
||||
type EntryModuleOption,
|
||||
type RuleSetOption,
|
||||
} from "~/api/document-types/document-types";
|
||||
import { getDocumentSubtypeGroupsMap, type DocumentSubtypeGroup } from "~/api/files/files-upload";
|
||||
import newStyles from "~/styles/pages/document-types_new.css?url";
|
||||
|
||||
export function links() {
|
||||
@@ -30,8 +27,7 @@ export const meta: MetaFunction = () => {
|
||||
interface LoaderData {
|
||||
entryModules: EntryModuleOption[];
|
||||
ruleSets: RuleSetOption[];
|
||||
editType: DocumentType | null;
|
||||
runtimeSubtypeGroups: DocumentSubtypeGroup[];
|
||||
editRoot: DocumentTypeRoot | null;
|
||||
frontendJWT?: string | null;
|
||||
}
|
||||
|
||||
@@ -41,132 +37,86 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const editId = url.searchParams.get("id");
|
||||
|
||||
const [modulesRes, setsRes] = await Promise.all([
|
||||
const [modulesRes, ruleSetsRes] = await Promise.all([
|
||||
getEntryModules(frontendJWT),
|
||||
getRuleSets(frontendJWT),
|
||||
]);
|
||||
const editRoot = editId ? (await getDocumentTypeRoot(Number(editId), frontendJWT)).data || null : null;
|
||||
|
||||
let editType: DocumentType | null = null;
|
||||
let runtimeSubtypeGroups: DocumentSubtypeGroup[] = [];
|
||||
if (editId) {
|
||||
const res = await getDocumentType(parseInt(editId), frontendJWT);
|
||||
editType = res.data || null;
|
||||
if (editType?.id) {
|
||||
const groupsRes = await getDocumentSubtypeGroupsMap([editType.id], frontendJWT);
|
||||
if ("data" in groupsRes && groupsRes.data) {
|
||||
runtimeSubtypeGroups = groupsRes.data[editType.id] || [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { entryModules: modulesRes.data || [], ruleSets: setsRes.data || [], editType, runtimeSubtypeGroups, frontendJWT };
|
||||
return {
|
||||
entryModules: modulesRes.data || [],
|
||||
ruleSets: ruleSetsRes.data || [],
|
||||
editRoot,
|
||||
frontendJWT,
|
||||
};
|
||||
}
|
||||
|
||||
export default function DocumentTypeNew() {
|
||||
export default function DocumentTypeRootEditor() {
|
||||
const navigate = useNavigate();
|
||||
const loaderData = useLoaderData<LoaderData>();
|
||||
const editType = loaderData.editType;
|
||||
const isEdit = !!editType;
|
||||
const editRoot = loaderData.editRoot;
|
||||
const isEdit = !!editRoot;
|
||||
|
||||
const [code, setCode] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [entryModuleId, setEntryModuleId] = useState<number | null>(null);
|
||||
const [selectedRuleSetIds, setSelectedRuleSetIds] = useState<number[]>([]);
|
||||
const [ruleSetKeyword, setRuleSetKeyword] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const selectedModule = loaderData.entryModules.find((m) => m.id === entryModuleId);
|
||||
const runtimeSubtypeGroups = loaderData.runtimeSubtypeGroups || [];
|
||||
const runtimeRootGroups = Array.from(
|
||||
new Map(
|
||||
runtimeSubtypeGroups
|
||||
.filter((group) => group.rootGroupId || group.rootGroupName)
|
||||
.map((group) => [group.rootGroupId || group.rootGroupName || group.id, {
|
||||
id: group.rootGroupId || null,
|
||||
name: group.rootGroupName || "未归属一级分组",
|
||||
}]),
|
||||
).values(),
|
||||
);
|
||||
const ruleSetsReadonly = isEdit && runtimeSubtypeGroups.length > 0;
|
||||
const selectedRuleSets = loaderData.ruleSets.filter((rs) => selectedRuleSetIds.includes(rs.id));
|
||||
const selectedUnavailableRuleSets = selectedRuleSets.filter((rs) => !rs.hasUsableVersion);
|
||||
const normalizedRuleSetKeyword = ruleSetKeyword.trim().toLowerCase();
|
||||
const filteredRuleSets = loaderData.ruleSets.filter((rs) => {
|
||||
if (!normalizedRuleSetKeyword) return true;
|
||||
return [
|
||||
rs.ruleName,
|
||||
rs.ruleType,
|
||||
rs.status,
|
||||
String(rs.id),
|
||||
String(rs.usableRuleCount || 0),
|
||||
rs.currentVersionId ? String(rs.currentVersionId) : "",
|
||||
rs.fallbackVersionId ? String(rs.fallbackVersionId) : "",
|
||||
].some((value) => value.toLowerCase().includes(normalizedRuleSetKeyword));
|
||||
});
|
||||
const completionCount = [!!code.trim(), !!name.trim(), entryModuleId !== null, selectedRuleSetIds.length > 0]
|
||||
.filter(Boolean).length;
|
||||
|
||||
useEffect(() => {
|
||||
if (editType) {
|
||||
setCode(editType.code || "");
|
||||
setName(editType.name || "");
|
||||
setDescription(editType.description || "");
|
||||
setEntryModuleId(editType.entryModuleId);
|
||||
setSelectedRuleSetIds(editType.ruleSetIds || []);
|
||||
if (editRoot) {
|
||||
setCode(editRoot.code || "");
|
||||
setName(editRoot.name || "");
|
||||
setDescription(editRoot.description || "");
|
||||
setEntryModuleId(editRoot.entryModuleId);
|
||||
}
|
||||
}, [editType]);
|
||||
}, [editRoot]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const errs: Record<string, string> = {};
|
||||
if (!code.trim()) errs.code = "编码不能为空";
|
||||
else if (!/^[a-zA-Z][a-zA-Z0-9_.]*$/.test(code.trim())) errs.code = "编码格式:字母开头,可含字母数字._";
|
||||
if (!name.trim()) errs.name = "名称不能为空";
|
||||
if (!ruleSetsReadonly && selectedUnavailableRuleSets.length > 0) {
|
||||
errs.ruleSetIds = "已选择的规则集中包含不可用于上传评查的项,请先确认可用规则数是否正常";
|
||||
}
|
||||
setErrors(errs);
|
||||
return Object.keys(errs).length === 0;
|
||||
};
|
||||
|
||||
const toggleRuleSet = (id: number) => {
|
||||
setSelectedRuleSetIds((prev) =>
|
||||
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
||||
const selectedModule = loaderData.entryModules.find((module) => module.id === entryModuleId);
|
||||
const selectedRuleSets = useMemo(
|
||||
() => loaderData.ruleSets.filter((ruleSet) => (editRoot?.ruleSetIds || []).includes(ruleSet.id)),
|
||||
[editRoot, loaderData.ruleSets],
|
||||
);
|
||||
const completionCount = [!!code.trim(), !!name.trim(), entryModuleId !== null].filter(Boolean).length;
|
||||
|
||||
const validate = () => {
|
||||
const nextErrors: Record<string, string> = {};
|
||||
if (!code.trim()) nextErrors.code = "编码不能为空";
|
||||
else if (!/^[a-zA-Z][a-zA-Z0-9_.]*$/.test(code.trim())) nextErrors.code = "编码格式:字母开头,可含字母数字._";
|
||||
if (!name.trim()) nextErrors.name = "名称不能为空";
|
||||
setErrors(nextErrors);
|
||||
return Object.keys(nextErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!validate()) return;
|
||||
|
||||
const payload = {
|
||||
code: code.trim(),
|
||||
name: name.trim(),
|
||||
description: description.trim() || null,
|
||||
entryModuleId,
|
||||
sortOrder: 0,
|
||||
isEnabled: true,
|
||||
};
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
if (isEdit && editType) {
|
||||
const dto: DocumentTypeUpdateDTO = {
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
entryModuleId,
|
||||
...(ruleSetsReadonly ? {} : { ruleSetIds: selectedRuleSetIds }),
|
||||
};
|
||||
const res = await updateDocumentType(editType.id, dto, loaderData.frontendJWT ?? undefined);
|
||||
if (res.error) { toastService.error(res.error); return; }
|
||||
toastService.success("文档类型已更新");
|
||||
if (isEdit && editRoot) {
|
||||
const result = await updateDocumentTypeRoot(editRoot.id, payload, loaderData.frontendJWT);
|
||||
if (result.error) throw new Error(result.error);
|
||||
toastService.success("一级文档类型已更新");
|
||||
} else {
|
||||
const dto: DocumentTypeCreateDTO = {
|
||||
code: code.trim(),
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
entryModuleId,
|
||||
ruleSetIds: selectedRuleSetIds,
|
||||
};
|
||||
const res = await createDocumentType(dto, loaderData.frontendJWT ?? undefined);
|
||||
if (res.error) { toastService.error(res.error); return; }
|
||||
toastService.success("文档类型已创建");
|
||||
const result = await createDocumentTypeRoot(payload, loaderData.frontendJWT);
|
||||
if (result.error) throw new Error(result.error);
|
||||
toastService.success("一级文档类型已创建");
|
||||
}
|
||||
navigate("/document-types");
|
||||
} catch (error) {
|
||||
toastService.error(error instanceof Error ? error.message : "保存失败");
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message || error?.response?.data?.msg || error?.message || "保存失败";
|
||||
toastService.error(message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -176,13 +126,13 @@ export default function DocumentTypeNew() {
|
||||
<div className="document-type-new-page">
|
||||
<div className="page-header">
|
||||
<div className="page-heading">
|
||||
<span className="page-kicker">{isEdit ? "文档类型编辑" : "文档类型配置"}</span>
|
||||
<span className="page-kicker">{isEdit ? "一级文档类型编辑" : "一级文档类型配置"}</span>
|
||||
<h2 className="page-title">
|
||||
<i className="ri-file-list-3-line"></i>
|
||||
{isEdit ? `编辑文档类型 — ${editType?.code}` : "新建文档类型"}
|
||||
{isEdit ? `编辑一级文档类型 — ${editRoot?.code}` : "新建一级文档类型"}
|
||||
</h2>
|
||||
<p className="page-subtitle">
|
||||
为上传入口绑定清晰的文档语义、汇总规则集和处理流向;实际运行时仍以评查点分组页中的“二级分组 → 规则集”绑定为准。
|
||||
这里维护的是文档类型的大类层级,也就是入口模块下的一级分组;具体业务子类型和规则集绑定统一在评查点分组管理里维护。
|
||||
</p>
|
||||
</div>
|
||||
<div className="header-overview">
|
||||
@@ -206,13 +156,13 @@ export default function DocumentTypeNew() {
|
||||
<div className="hero-metrics">
|
||||
<div className="hero-metric-card">
|
||||
<span className="hero-metric-label">配置完成度</span>
|
||||
<strong>{completionCount}/4</strong>
|
||||
<small>基础信息、入口、规则集逐步补齐</small>
|
||||
<strong>{completionCount}/3</strong>
|
||||
<small>基础信息与入口模块逐步补齐</small>
|
||||
</div>
|
||||
<div className="hero-metric-card">
|
||||
<span className="hero-metric-label">已关联规则集</span>
|
||||
<strong>{selectedRuleSetIds.length} 个</strong>
|
||||
<small>{selectedRuleSetIds.length > 0 ? "已进入评查链路" : "建议按业务场景精确选择"}</small>
|
||||
<span className="hero-metric-label">下属二级分组</span>
|
||||
<strong>{editRoot?.childGroupCount || 0} 个</strong>
|
||||
<small>{isEdit ? "运行子类型维护在分组管理页" : "创建后再去拆分子类型"}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,25 +175,9 @@ export default function DocumentTypeNew() {
|
||||
<div className="section-heading">
|
||||
<div>
|
||||
<span className="section-kicker">Step 01</span>
|
||||
<h3>基础标识</h3>
|
||||
</div>
|
||||
<p>先定义业务识别码与名称,确保后续抽取、归档、评查引用一致。</p>
|
||||
</div>
|
||||
<div className="section-intro-card">
|
||||
<div className="section-intro-item">
|
||||
<i className="ri-fingerprint-line"></i>
|
||||
<div>
|
||||
<strong>编码保持稳定</strong>
|
||||
<span>编码创建后建议长期复用,避免和展示文案耦合。</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section-intro-item">
|
||||
<i className="ri-text"></i>
|
||||
<div>
|
||||
<strong>名称面向使用者</strong>
|
||||
<span>名称会直接出现在上传入口与评查流程,尽量让业务人员一眼看懂。</span>
|
||||
</div>
|
||||
<h3>一级大类标识</h3>
|
||||
</div>
|
||||
<p>先定义一级文档类型的编码与名称,例如合同、行政卷宗、后续新增业务大类。</p>
|
||||
</div>
|
||||
|
||||
<div className="form-row two-column">
|
||||
@@ -252,27 +186,32 @@ export default function DocumentTypeNew() {
|
||||
<input
|
||||
type="text"
|
||||
className={`form-input ${errors.code ? "error" : ""}`}
|
||||
placeholder="如: contract.sale"
|
||||
placeholder="如: root.contract"
|
||||
value={code}
|
||||
onChange={(e) => { setCode(e.target.value); setErrors({ ...errors, code: "" }); }}
|
||||
onChange={(event) => {
|
||||
setCode(event.target.value);
|
||||
setErrors((current) => ({ ...current, code: "" }));
|
||||
}}
|
||||
disabled={isEdit}
|
||||
/>
|
||||
{errors.code && <span className="form-error">{errors.code}</span>}
|
||||
{!errors.code && (
|
||||
<span className="form-hint">{isEdit ? "编码创建后不可修改" : "建议使用业务域.场景名,便于接口与导航复用"}</span>
|
||||
)}
|
||||
{!errors.code && <span className="form-hint">{isEdit ? "一级编码创建后不可修改" : "建议使用 root.xxx 语义,明确它是一级大类"}</span>}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="required">类型名称</label>
|
||||
<input
|
||||
type="text"
|
||||
className={`form-input ${errors.name ? "error" : ""}`}
|
||||
placeholder="如: 通用买卖合同"
|
||||
placeholder="如: 合同"
|
||||
value={name}
|
||||
onChange={(e) => { setName(e.target.value); setErrors({ ...errors, name: "" }); }}
|
||||
onChange={(event) => {
|
||||
setName(event.target.value);
|
||||
setErrors((current) => ({ ...current, name: "" }));
|
||||
}}
|
||||
/>
|
||||
{errors.name && <span className="form-error">{errors.name}</span>}
|
||||
{!errors.name && <span className="form-hint">名称会直接出现在上传入口、文档详情和评查流程里</span>}
|
||||
{!errors.name && <span className="form-hint">名称就是前端入口里看到的一级业务大类</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -280,12 +219,12 @@ export default function DocumentTypeNew() {
|
||||
<label>描述</label>
|
||||
<textarea
|
||||
className="form-textarea"
|
||||
placeholder="补充这个文档类型的适用业务场景、上传说明、抽取注意事项"
|
||||
placeholder="补充这个一级文档类型覆盖的业务范围"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
onChange={(event) => setDescription(event.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
<span className="form-hint">建议写清这个类型的用途,方便后续运营和规则维护人员理解。</span>
|
||||
<span className="form-hint">例如“合同业务大类”“行政卷宗业务大类”。</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -293,9 +232,9 @@ export default function DocumentTypeNew() {
|
||||
<div className="section-heading">
|
||||
<div>
|
||||
<span className="section-kicker">Step 02</span>
|
||||
<h3>入口与流程绑定</h3>
|
||||
<h3>入口模块绑定</h3>
|
||||
</div>
|
||||
<p>把文档类型挂到正确入口,让前端导航、上传动作和后端处理链条保持一致。</p>
|
||||
<p>一级文档类型先绑定入口模块,用户从对应模块进入后,再由二级分组承接具体业务类型。</p>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
@@ -303,206 +242,90 @@ export default function DocumentTypeNew() {
|
||||
<select
|
||||
className="form-select"
|
||||
value={entryModuleId ?? ""}
|
||||
onChange={(e) => setEntryModuleId(e.target.value ? parseInt(e.target.value) : null)}
|
||||
onChange={(event) => setEntryModuleId(event.target.value ? Number(event.target.value) : null)}
|
||||
>
|
||||
<option value="">不关联</option>
|
||||
{loaderData.entryModules.map((m) => (
|
||||
<option key={m.id} value={m.id}>{m.name}</option>
|
||||
{loaderData.entryModules.map((module) => (
|
||||
<option key={module.id} value={module.id}>{module.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="form-hint">如果绑定入口模块,用户会在对应入口中更自然地发现该文档类型。</span>
|
||||
<span className="form-hint">例如“合同管理”绑定到一级大类“合同”,“案卷智能评查”绑定到一级大类“行政卷宗”。</span>
|
||||
</div>
|
||||
|
||||
<div className="binding-preview">
|
||||
<div className="binding-preview-label">当前归属大类</div>
|
||||
<div className="binding-preview-label">当前入口模块</div>
|
||||
<div className="binding-preview-value">
|
||||
<i className="ri-route-line"></i>
|
||||
<span>{selectedModule?.name || "未绑定入口模块,上传入口不会主动露出此类型"}</span>
|
||||
<span>{selectedModule?.name || "未绑定入口模块"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rule-set-warning">
|
||||
<i className="ri-git-merge-line"></i>
|
||||
<div>
|
||||
<strong>这里绑定的是文档类型所属大类,不是二级分组</strong>
|
||||
<span>
|
||||
文档类型先归属到入口模块 / 一级大类(例如:合同、卷宗),
|
||||
再由评查点分组页把它映射到具体运行子类型(例如:建设工程合同、借款合同、许可-停业办理)。
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isEdit ? (
|
||||
<div className="rule-set-warning">
|
||||
<i className="ri-node-tree"></i>
|
||||
<div>
|
||||
<strong>当前运行链路摘要</strong>
|
||||
<span>
|
||||
{runtimeSubtypeGroups.length > 0
|
||||
? `当前文档类型在评查点分组中已映射到 ${runtimeSubtypeGroups.length} 个运行子类型;上传时会先命中所属大类,再按所选子类型决定实际规则集。`
|
||||
: "当前文档类型还没有在评查点分组中挂到任何二级分组,上传时无法形成完整的新链路。"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{isEdit && runtimeSubtypeGroups.length > 0 ? (
|
||||
<div className="selected-rule-sets-panel">
|
||||
<div className="selected-panel-header">
|
||||
<div>
|
||||
<strong>运行子类型映射</strong>
|
||||
<span>这里只展示实际会命中的二级分组;文档类型归属仍以上面的入口模块 / 一级大类为准。</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rule-set-warning">
|
||||
<div className="binding-preview">
|
||||
<div className="binding-preview-label">当前一级分组</div>
|
||||
<div className="binding-preview-value">
|
||||
<i className="ri-stack-line"></i>
|
||||
<span>{name.trim() || "待填写一级文档类型名称"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rule-set-warning">
|
||||
<i className="ri-information-line"></i>
|
||||
<div>
|
||||
<strong>一级大类</strong>
|
||||
<span>
|
||||
{runtimeRootGroups.map((group) => group.name).join("、")}
|
||||
{selectedModule?.name ? ` · 所属入口模块:${selectedModule.name}` : ""}
|
||||
</span>
|
||||
<strong>这里不再直接维护具体合同类型或卷宗子类型</strong>
|
||||
<span>建设工程合同、买卖合同、行政许可-新办、行政许可-停业等具体业务类型,统一在“评查点分组管理”中作为二级分组维护。</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="selected-rule-grid">
|
||||
{runtimeSubtypeGroups.map((group) => (
|
||||
<div key={group.id} className="selected-rule-card">
|
||||
<div className="selected-rule-main">
|
||||
<strong>{group.displayName || group.name}</strong>
|
||||
<span>
|
||||
一级分组:{group.rootGroupName || "未归属一级分组"}
|
||||
{group.entryModuleName ? ` · 入口模块:${group.entryModuleName}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
<span className="selected-rule-meta">运行子类型 · {group.code}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className="form-section">
|
||||
<div className="section-heading">
|
||||
<div>
|
||||
<span className="section-kicker">Step 03</span>
|
||||
<h3>汇总规则集</h3>
|
||||
<h3>规则汇总只读预览</h3>
|
||||
</div>
|
||||
<p>这里展示的是文档类型维度的汇总规则集,用于兼容旧页面与快速总览;如果同一文档类型拆成多个二级分组,实际上传命中仍以分组页配置为准。</p>
|
||||
<p>一级文档类型本身不直接绑定规则集;这里只读展示该一级分组下二级分组汇总出的规则集。</p>
|
||||
</div>
|
||||
|
||||
<div className="rule-set-warning">
|
||||
<i className={ruleSetsReadonly ? "ri-lock-line" : "ri-information-line"}></i>
|
||||
<div>
|
||||
<strong>{ruleSetsReadonly ? "当前已收口为只读汇总" : "这是汇总视图,不是最终运行绑定"}</strong>
|
||||
<span>
|
||||
{ruleSetsReadonly
|
||||
? "当前文档类型已经接入评查点分组新链路。这里仅展示汇总结果,不再作为最终运行绑定编辑入口;请到“评查点分组管理”维护二级分组下的实际规则集。"
|
||||
: "当一个文档类型下存在多个二级分组时,这里看到的是所有二级分组规则集的并集汇总;具体到某次上传,只会命中所选子类型下的规则集。"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rule-set-toolbar">
|
||||
<div className="rule-set-toolbar-main">
|
||||
<span className="rule-set-counter">
|
||||
<i className="ri-checkbox-circle-line"></i>
|
||||
已选择 {selectedRuleSetIds.length} / {loaderData.ruleSets.length}
|
||||
</span>
|
||||
<span className="form-hint">
|
||||
{ruleSetsReadonly
|
||||
? "当前只读展示的是文档类型层汇总结果;上传时将优先按入口模块下命中的二级分组执行评查。"
|
||||
: "这里用于维护文档类型层的汇总结果;上传时若已拆分子类型,后端会优先按二级分组绑定执行评查。"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="rule-set-search">
|
||||
<i className="ri-search-line"></i>
|
||||
<input
|
||||
type="text"
|
||||
value={ruleSetKeyword}
|
||||
onChange={(e) => setRuleSetKeyword(e.target.value)}
|
||||
placeholder="搜索规则集名称、类型、ID、版本"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{ruleSetsReadonly ? (
|
||||
<div className="readonly-guide-card">
|
||||
<div>
|
||||
<strong>如需调整实际运行规则</strong>
|
||||
<span>请进入“评查点分组管理”,在对应一级分组 / 二级分组下编辑规则集绑定。</span>
|
||||
<strong>实际运行规则维护入口</strong>
|
||||
<span>请进入“评查点分组管理”,在当前一级分组下面新增/编辑二级分组,再给二级分组绑定实际运行规则集。</span>
|
||||
</div>
|
||||
<Button type="default" onClick={() => navigate("/rule-groups")} disabled={saving}>
|
||||
打开评查点分组
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{selectedUnavailableRuleSets.length > 0 && (
|
||||
<div className="rule-set-warning">
|
||||
<i className="ri-error-warning-line"></i>
|
||||
<div>
|
||||
<strong>当前选择里存在不可执行的规则集</strong>
|
||||
<span>
|
||||
{selectedUnavailableRuleSets.map((item) => item.ruleName).join("、")}
|
||||
{" "}当前不可用于评查;若直接上传,后端会拒绝发起评查。
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rule-set-checklist">
|
||||
{loaderData.ruleSets.length === 0 ? (
|
||||
{!isEdit ? (
|
||||
<div className="empty-rule-set">
|
||||
<i className="ri-node-tree"></i>
|
||||
<p>创建后再维护二级分组</p>
|
||||
<span>一级大类保存成功后,再去评查点分组页拆分具体业务子类型与规则集。</span>
|
||||
</div>
|
||||
) : selectedRuleSets.length === 0 ? (
|
||||
<div className="empty-rule-set">
|
||||
<i className="ri-inbox-archive-line"></i>
|
||||
<p>暂无可选规则集</p>
|
||||
<span>请先在规则管理中创建可用于文档评查的规则集。</span>
|
||||
</div>
|
||||
) : filteredRuleSets.length === 0 ? (
|
||||
<div className="empty-rule-set compact">
|
||||
<i className="ri-search-eye-line"></i>
|
||||
<p>没有匹配的规则集</p>
|
||||
<span>请尝试更换关键词,或清空搜索后重新选择。</span>
|
||||
<p>当前一级分组下暂无汇总规则集</p>
|
||||
<span>说明还没有给下属二级分组挂规则集。</span>
|
||||
</div>
|
||||
) : (
|
||||
filteredRuleSets.map((rs) => (
|
||||
<label
|
||||
key={rs.id}
|
||||
className={`rule-set-item ${selectedRuleSetIds.includes(rs.id) ? "checked" : ""} ${rs.hasUsableVersion ? "" : "unavailable"}`}
|
||||
>
|
||||
<div className="rule-set-checkbox-wrap">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedRuleSetIds.includes(rs.id)}
|
||||
onChange={() => toggleRuleSet(rs.id)}
|
||||
disabled={ruleSetsReadonly}
|
||||
/>
|
||||
</div>
|
||||
selectedRuleSets.map((ruleSet) => (
|
||||
<div key={ruleSet.id} className="rule-set-item checked">
|
||||
<div className="rule-set-content">
|
||||
<div className="rule-set-topline">
|
||||
<span className="rule-set-name">{rs.ruleName}</span>
|
||||
<span className={`rule-set-status ${rs.status}`}>{rs.status}</span>
|
||||
<span className="rule-set-name">{ruleSet.ruleName}</span>
|
||||
<span className={`rule-set-status ${ruleSet.status}`}>{ruleSet.status}</span>
|
||||
</div>
|
||||
<div className="rule-set-meta">
|
||||
<span className="rule-set-type">{rs.ruleType}</span>
|
||||
<span className="rule-set-id">规则集 ID #{rs.id}</span>
|
||||
<span className={`rule-set-version-badge ${rs.hasUsableVersion ? "ok" : "missing"}`}>
|
||||
{rs.hasUsableVersion
|
||||
? `可用规则数:${rs.usableRuleCount || 0}`
|
||||
: "可用规则数:0"}
|
||||
<span className="rule-set-type">{ruleSet.ruleType}</span>
|
||||
<span className="rule-set-id">规则集 ID #{ruleSet.id}</span>
|
||||
<span className={`rule-set-version-badge ${ruleSet.hasUsableVersion ? "ok" : "missing"}`}>
|
||||
{ruleSet.hasUsableVersion ? `可用规则数:${ruleSet.usableRuleCount || 0}` : "可用规则数:0"}
|
||||
</span>
|
||||
</div>
|
||||
{!rs.hasUsableVersion && (
|
||||
<div className="rule-set-inline-warning">
|
||||
<i className="ri-alarm-warning-line"></i>
|
||||
<span>该规则集当前没有可用规则,绑定后仍无法执行上传评查。</span>
|
||||
</div>
|
||||
)}
|
||||
{rs.hasUsableVersion && !rs.currentVersionId && rs.fallbackVersionId && (
|
||||
<div className="rule-set-inline-warning soft">
|
||||
<i className="ri-information-line"></i>
|
||||
<span>当前规则可用,但数据来源仍是回退链路,建议在规则管理中补齐正式配置。</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
@@ -528,7 +351,7 @@ export default function DocumentTypeNew() {
|
||||
<div className="summary-grid">
|
||||
<div className="summary-item">
|
||||
<span className="summary-label">当前模式</span>
|
||||
<strong>{isEdit ? "编辑既有类型" : "新建类型"}</strong>
|
||||
<strong>{isEdit ? "编辑一级大类" : "新建一级大类"}</strong>
|
||||
</div>
|
||||
<div className="summary-item">
|
||||
<span className="summary-label">类型编码</span>
|
||||
@@ -543,17 +366,17 @@ export default function DocumentTypeNew() {
|
||||
<strong>{selectedModule?.name || "未绑定"}</strong>
|
||||
</div>
|
||||
<div className="summary-item full">
|
||||
<span className="summary-label">已关联规则集</span>
|
||||
<strong>{selectedRuleSetIds.length} 个</strong>
|
||||
<span className="summary-label">下属二级分组</span>
|
||||
<strong>{editRoot?.childGroupCount || 0} 个</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="summary-progress">
|
||||
<div className="summary-progress-topline">
|
||||
<span>表单完成度</span>
|
||||
<strong>{completionCount * 25}%</strong>
|
||||
<strong>{Math.round((completionCount / 3) * 100)}%</strong>
|
||||
</div>
|
||||
<div className="summary-progress-bar">
|
||||
<span style={{ width: `${completionCount * 25}%` }}></span>
|
||||
<span style={{ width: `${(completionCount / 3) * 100}%` }}></span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -564,36 +387,18 @@ export default function DocumentTypeNew() {
|
||||
<h3>绑定结果预览</h3>
|
||||
</div>
|
||||
<div className="selection-stack">
|
||||
<div className="selection-item">
|
||||
<span className="selection-label">一级大类</span>
|
||||
<strong>{name.trim() || "暂未指定"}</strong>
|
||||
</div>
|
||||
<div className="selection-item">
|
||||
<span className="selection-label">入口归属</span>
|
||||
<strong>{selectedModule?.name || "暂未指定"}</strong>
|
||||
</div>
|
||||
<div className="selection-item">
|
||||
<span className="selection-label">规则集清单</span>
|
||||
{selectedRuleSets.length > 0 ? (
|
||||
<div className="selection-tags">
|
||||
{selectedRuleSets.slice(0, 4).map((ruleSet) => (
|
||||
<span
|
||||
key={ruleSet.id}
|
||||
className={`selection-tag ${ruleSet.hasUsableVersion ? "" : "warning"}`}
|
||||
>
|
||||
{ruleSet.ruleName}
|
||||
</span>
|
||||
))}
|
||||
{selectedRuleSets.length > 4 && (
|
||||
<span className="selection-tag muted">+{selectedRuleSets.length - 4}</span>
|
||||
)}
|
||||
<span className="selection-label">规则汇总</span>
|
||||
<strong>{isEdit ? `${selectedRuleSets.length} 个汇总规则集` : "创建后由二级分组汇总"}</strong>
|
||||
</div>
|
||||
) : (
|
||||
<strong>暂未选择</strong>
|
||||
)}
|
||||
</div>
|
||||
{selectedUnavailableRuleSets.length > 0 && (
|
||||
<div className="selection-item danger">
|
||||
<span className="selection-label">上传风险</span>
|
||||
<strong>当前绑定无法直接触发评查</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -603,9 +408,9 @@ export default function DocumentTypeNew() {
|
||||
<h3>建议做法</h3>
|
||||
</div>
|
||||
<ul className="guide-list">
|
||||
<li>编码保持稳定,不要混入显示文案,便于后端接口与权限配置长期复用。</li>
|
||||
<li>描述尽量写业务边界,例如“主合同”“补充协议”“付款附件”等,避免上传误选。</li>
|
||||
<li>{ruleSetsReadonly ? "当前类型的实际运行规则请统一在评查点分组页维护,避免这里和分组页双写冲突。" : "规则集宁可精简,也不要把无关规则打包给所有文档类型,避免误报过多。"}</li>
|
||||
<li>一级文档类型只表达业务大类,例如合同、行政卷宗,不要在这里写建设工程合同这类具体子类型。</li>
|
||||
<li>入口模块与一级文档类型一一对应,用户从入口进入后,再由二级分组承接具体运行链路。</li>
|
||||
<li>具体规则集绑定统一放在评查点分组管理,避免这里和二级分组页双写冲突。</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</aside>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { redirect, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "规则管理 - 中国烟草AI合同及卷宗审核系统" },
|
||||
{
|
||||
name: "rules-sets",
|
||||
content: "兼容旧版规则管理入口,自动跳转到新版规则维护页"
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
hideBreadcrumb: true,
|
||||
};
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const query = url.searchParams.toString();
|
||||
throw redirect(query ? `/rulesTest/list?${query}` : "/rulesTest/list");
|
||||
}
|
||||
|
||||
export default function RulesSetsRedirect() {
|
||||
return null;
|
||||
}
|
||||
@@ -127,14 +127,6 @@ export const permissionRouteAliasGroups = [
|
||||
{ input: '/rules/new', output: '/rules' },
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '^/rules/sets(?=/|$)',
|
||||
target: '/rules',
|
||||
note: '旧版规则管理入口复用规则管理主菜单权限,并跳转到新版规则维护页。',
|
||||
examples: [
|
||||
{ input: '/rules/sets', output: '/rules' },
|
||||
],
|
||||
},
|
||||
{
|
||||
source: '^/documents/list(?=/|$)',
|
||||
target: '/documents',
|
||||
|
||||
Reference in New Issue
Block a user