fix: clarify document type grouping hierarchy
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
|||||||
type DocumentType,
|
type DocumentType,
|
||||||
type EntryModuleOption,
|
type EntryModuleOption,
|
||||||
} from "~/api/document-types/document-types";
|
} from "~/api/document-types/document-types";
|
||||||
|
import { getDocumentSubtypeGroups, type DocumentSubtypeGroup } from "~/api/files/files-upload";
|
||||||
import documentTypesStyles from "~/styles/pages/document-types_index.css?url";
|
import documentTypesStyles from "~/styles/pages/document-types_index.css?url";
|
||||||
|
|
||||||
export function links() {
|
export function links() {
|
||||||
@@ -27,6 +28,7 @@ export const meta: MetaFunction = () => {
|
|||||||
interface LoaderData {
|
interface LoaderData {
|
||||||
types: DocumentType[];
|
types: DocumentType[];
|
||||||
entryModules: EntryModuleOption[];
|
entryModules: EntryModuleOption[];
|
||||||
|
subtypeGroupsByTypeId: Record<number, DocumentSubtypeGroup[]>;
|
||||||
frontendJWT?: string | null;
|
frontendJWT?: string | null;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
@@ -41,13 +43,22 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
getEntryModules(frontendJWT),
|
getEntryModules(frontendJWT),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const types = typesRes.data?.types || [];
|
||||||
|
const subtypeGroupEntries = await Promise.all(
|
||||||
|
types.map(async (type) => {
|
||||||
|
const groupsRes = await getDocumentSubtypeGroups(type.id, frontendJWT, type.entryModuleId || undefined);
|
||||||
|
return [type.id, "data" in groupsRes && groupsRes.data ? groupsRes.data : []] as const;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
types: typesRes.data?.types || [],
|
types,
|
||||||
entryModules: modulesRes.data || [],
|
entryModules: modulesRes.data || [],
|
||||||
|
subtypeGroupsByTypeId: Object.fromEntries(subtypeGroupEntries),
|
||||||
frontendJWT,
|
frontendJWT,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { types: [], entryModules: [], error: "加载失败" };
|
return { types: [], entryModules: [], subtypeGroupsByTypeId: {}, error: "加载失败" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +67,7 @@ export default function DocumentTypesIndex() {
|
|||||||
const loaderData = useLoaderData<LoaderData>();
|
const loaderData = useLoaderData<LoaderData>();
|
||||||
const [types, setTypes] = useState<DocumentType[]>(loaderData.types || []);
|
const [types, setTypes] = useState<DocumentType[]>(loaderData.types || []);
|
||||||
const entryModules = loaderData.entryModules || [];
|
const entryModules = loaderData.entryModules || [];
|
||||||
|
const subtypeGroupsByTypeId = loaderData.subtypeGroupsByTypeId || {};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTypes(loaderData.types || []);
|
setTypes(loaderData.types || []);
|
||||||
@@ -66,6 +78,17 @@ export default function DocumentTypesIndex() {
|
|||||||
return entryModules.find((m) => m.id === id)?.name || `#${id}`;
|
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 handleDelete = async (type: DocumentType) => {
|
||||||
const confirmed = window.confirm(`确定要删除文档类型「${type.name}」吗?`);
|
const confirmed = window.confirm(`确定要删除文档类型「${type.name}」吗?`);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
@@ -103,7 +126,8 @@ export default function DocumentTypesIndex() {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>编码</th>
|
<th>编码</th>
|
||||||
<th>名称</th>
|
<th>名称</th>
|
||||||
<th>入口模块</th>
|
<th>所属大类</th>
|
||||||
|
<th>运行子类型映射</th>
|
||||||
<th>汇总规则集</th>
|
<th>汇总规则集</th>
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
@@ -113,8 +137,31 @@ export default function DocumentTypesIndex() {
|
|||||||
{types.map((type) => (
|
{types.map((type) => (
|
||||||
<tr key={type.id}>
|
<tr key={type.id}>
|
||||||
<td><code>{type.code}</code></td>
|
<td><code>{type.code}</code></td>
|
||||||
<td>{type.name}</td>
|
<td>
|
||||||
<td>{getEntryModuleName(type.entryModuleId)}</td>
|
<div className="type-cell">
|
||||||
|
<strong>{type.name}</strong>
|
||||||
|
<span>文档类型</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="type-cell">
|
||||||
|
<strong>{getEntryModuleName(type.entryModuleId)}</strong>
|
||||||
|
<span>{getRootGroupSummary(type.id)}</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>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span className="tag">{type.ruleSetIds?.length || 0} 个规则集</span>
|
<span className="tag">{type.ruleSetIds?.length || 0} 个规则集</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -79,6 +79,16 @@ export default function DocumentTypeNew() {
|
|||||||
|
|
||||||
const selectedModule = loaderData.entryModules.find((m) => m.id === entryModuleId);
|
const selectedModule = loaderData.entryModules.find((m) => m.id === entryModuleId);
|
||||||
const runtimeSubtypeGroups = loaderData.runtimeSubtypeGroups || [];
|
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 ruleSetsReadonly = isEdit && runtimeSubtypeGroups.length > 0;
|
||||||
const selectedRuleSets = loaderData.ruleSets.filter((rs) => selectedRuleSetIds.includes(rs.id));
|
const selectedRuleSets = loaderData.ruleSets.filter((rs) => selectedRuleSetIds.includes(rs.id));
|
||||||
const selectedUnavailableRuleSets = selectedRuleSets.filter((rs) => !rs.hasUsableVersion);
|
const selectedUnavailableRuleSets = selectedRuleSets.filter((rs) => !rs.hasUsableVersion);
|
||||||
@@ -304,13 +314,24 @@ export default function DocumentTypeNew() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="binding-preview">
|
<div className="binding-preview">
|
||||||
<div className="binding-preview-label">当前路由预览</div>
|
<div className="binding-preview-label">当前归属大类</div>
|
||||||
<div className="binding-preview-value">
|
<div className="binding-preview-value">
|
||||||
<i className="ri-route-line"></i>
|
<i className="ri-route-line"></i>
|
||||||
<span>{selectedModule?.name || "未绑定入口模块,上传入口不会主动露出此类型"}</span>
|
<span>{selectedModule?.name || "未绑定入口模块,上传入口不会主动露出此类型"}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="rule-set-warning">
|
||||||
|
<i className="ri-git-merge-line"></i>
|
||||||
|
<div>
|
||||||
|
<strong>这里绑定的是文档类型所属大类,不是二级分组</strong>
|
||||||
|
<span>
|
||||||
|
文档类型先归属到入口模块 / 一级大类(例如:合同、卷宗),
|
||||||
|
再由评查点分组页把它映射到具体运行子类型(例如:建设工程合同、借款合同、许可-停业办理)。
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isEdit ? (
|
{isEdit ? (
|
||||||
<div className="rule-set-warning">
|
<div className="rule-set-warning">
|
||||||
<i className="ri-node-tree"></i>
|
<i className="ri-node-tree"></i>
|
||||||
@@ -318,7 +339,7 @@ export default function DocumentTypeNew() {
|
|||||||
<strong>当前运行链路摘要</strong>
|
<strong>当前运行链路摘要</strong>
|
||||||
<span>
|
<span>
|
||||||
{runtimeSubtypeGroups.length > 0
|
{runtimeSubtypeGroups.length > 0
|
||||||
? `当前文档类型在评查点分组中已挂到 ${runtimeSubtypeGroups.length} 个二级分组;上传时会先按入口模块和子类型命中,再决定实际规则集。`
|
? `当前文档类型在评查点分组中已映射到 ${runtimeSubtypeGroups.length} 个运行子类型;上传时会先命中所属大类,再按所选子类型决定实际规则集。`
|
||||||
: "当前文档类型还没有在评查点分组中挂到任何二级分组,上传时无法形成完整的新链路。"}
|
: "当前文档类型还没有在评查点分组中挂到任何二级分组,上传时无法形成完整的新链路。"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -328,8 +349,18 @@ export default function DocumentTypeNew() {
|
|||||||
<div className="selected-rule-sets-panel">
|
<div className="selected-rule-sets-panel">
|
||||||
<div className="selected-panel-header">
|
<div className="selected-panel-header">
|
||||||
<div>
|
<div>
|
||||||
<strong>已关联的运行子类型</strong>
|
<strong>运行子类型映射</strong>
|
||||||
<span>这里只展示实际会命中的二级分组,最终规则仍以分组页绑定为准。</span>
|
<span>这里只展示实际会命中的二级分组;文档类型归属仍以上面的入口模块 / 一级大类为准。</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rule-set-warning">
|
||||||
|
<i className="ri-stack-line"></i>
|
||||||
|
<div>
|
||||||
|
<strong>一级大类</strong>
|
||||||
|
<span>
|
||||||
|
{runtimeRootGroups.map((group) => group.name).join("、")}
|
||||||
|
{selectedModule?.name ? ` · 所属入口模块:${selectedModule.name}` : ""}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="selected-rule-grid">
|
<div className="selected-rule-grid">
|
||||||
@@ -337,9 +368,12 @@ export default function DocumentTypeNew() {
|
|||||||
<div key={group.id} className="selected-rule-card">
|
<div key={group.id} className="selected-rule-card">
|
||||||
<div className="selected-rule-main">
|
<div className="selected-rule-main">
|
||||||
<strong>{group.displayName || group.name}</strong>
|
<strong>{group.displayName || group.name}</strong>
|
||||||
<span>{group.rootGroupName || "未归属一级分组"}{group.entryModuleName ? ` · ${group.entryModuleName}` : ""}</span>
|
<span>
|
||||||
|
一级分组:{group.rootGroupName || "未归属一级分组"}
|
||||||
|
{group.entryModuleName ? ` · 入口模块:${group.entryModuleName}` : ""}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="selected-rule-meta">{group.code}</span>
|
<span className="selected-rule-meta">运行子类型 · {group.code}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,6 +62,22 @@
|
|||||||
@apply flex flex-wrap gap-1 max-w-md;
|
@apply flex flex-wrap gap-1 max-w-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.document-types-page .type-cell {
|
||||||
|
@apply flex flex-col gap-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-types-page .type-cell strong {
|
||||||
|
@apply text-sm font-medium text-gray-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-types-page .type-cell span {
|
||||||
|
@apply text-xs text-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-types-page .subtle-text {
|
||||||
|
@apply text-xs text-gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
.document-types-page .operation-btn {
|
.document-types-page .operation-btn {
|
||||||
@apply inline-flex items-center text-sm px-2 py-1 rounded hover:bg-gray-100;
|
@apply inline-flex items-center text-sm px-2 py-1 rounded hover:bg-gray-100;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user