diff --git a/app/api/auth/user-routes.ts b/app/api/auth/user-routes.ts index 630bd57..accad36 100644 --- a/app/api/auth/user-routes.ts +++ b/app/api/auth/user-routes.ts @@ -1024,14 +1024,24 @@ function buildFallbackRoutes(roleKey: string): { const mappedRoleKey = mapUserRoleToRoleKey(roleKey); const fallbackMenus = FALLBACK_MENU_DATA[mappedRoleKey] || FALLBACK_MENU_DATA.common; const permissionMap: Record = {}; + const safeFallbackMenus = stripDisallowedFallbackRoutes(fallbackMenus); return { success: true, - data: normalizeMenuStructure(fallbackMenus.filter(item => isMinimalMenuPath(item.path))), + data: normalizeMenuStructure(safeFallbackMenus.filter(item => isMinimalMenuPath(item.path))), permissionMap, }; } +function stripDisallowedFallbackRoutes(menuItems: MenuItem[]): MenuItem[] { + return menuItems + .filter((item) => item.path !== '/rule-groups') + .map((item) => ({ + ...item, + children: item.children ? stripDisallowedFallbackRoutes(item.children) : undefined, + })); +} + function isLegacyRuleSetsMenu(path: string | undefined): boolean { return path === '/rules/sets'; } @@ -1059,41 +1069,5 @@ function normalizeMenuStructure(menuItems: MenuItem[]): MenuItem[] { const dedupedTopLevelItems = clonedMenuItems.filter(item => !nestedPathSet.has(item.path)); - const ruleManagement = dedupedTopLevelItems.find(item => item.path === '/rules'); - const systemSettings = dedupedTopLevelItems.find(item => item.path === '/settings'); - const syntheticRuleGroupsMenu: MenuItem = { - id: 'rule-groups', - title: '规则组导航', - path: '/rule-groups', - icon: 'ri-folder-open-line', - order: 1, - }; - - let ruleGroupsMenu: MenuItem = syntheticRuleGroupsMenu; - - if (ruleManagement?.children?.length) { - const ruleGroupIndex = ruleManagement.children.findIndex(child => child.path === '/rule-groups'); - if (ruleGroupIndex !== -1) { - const [existingRuleGroupsMenu] = ruleManagement.children.splice(ruleGroupIndex, 1); - ruleGroupsMenu = existingRuleGroupsMenu; - ruleManagement.children = ruleManagement.children - .map((child, index) => ({ ...child, order: index + 1 })) - .sort((a, b) => a.order - b.order); - } - } - - if (!systemSettings) { - return dedupedTopLevelItems; - } - - const settingsChildren = systemSettings.children ? [...systemSettings.children] : []; - if (!settingsChildren.some(child => child.path === '/rule-groups')) { - settingsChildren.unshift({ ...ruleGroupsMenu, order: 1 }); - } - - systemSettings.children = settingsChildren - .map((child, index) => ({ ...child, order: index + 1 })) - .sort((a, b) => a.order - b.order); - return dedupedTopLevelItems; } diff --git a/app/routes/document-types._index.tsx b/app/routes/document-types._index.tsx index 715b56d..36267fb 100644 --- a/app/routes/document-types._index.tsx +++ b/app/routes/document-types._index.tsx @@ -26,7 +26,10 @@ interface LoaderData { export async function loader({ request }: LoaderFunctionArgs) { try { const { getUserSession } = await import("~/api/login/auth.server"); - const { frontendJWT } = await getUserSession(request); + const { frontendJWT, userInfo } = await getUserSession(request); + const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server"); + + await requireRoutePermission("/document-types", userInfo?.role || "", frontendJWT || undefined); const rootsRes = await getDocumentTypeRoots({}, frontendJWT); return { diff --git a/app/routes/document-types.new.tsx b/app/routes/document-types.new.tsx index 8f6a2bb..2373c24 100644 --- a/app/routes/document-types.new.tsx +++ b/app/routes/document-types.new.tsx @@ -33,7 +33,9 @@ interface LoaderData { export async function loader({ request }: LoaderFunctionArgs) { const { getUserSession } = await import("~/api/login/auth.server"); - const { frontendJWT } = await getUserSession(request); + const { frontendJWT, userInfo } = await getUserSession(request); + const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server"); + await requireRoutePermission("/document-types/new", userInfo?.role || "", frontendJWT || undefined); const url = new URL(request.url); const editId = url.searchParams.get("id"); diff --git a/app/routes/rule-groups._index.tsx b/app/routes/rule-groups._index.tsx index 99663ae..1ca0d28 100644 --- a/app/routes/rule-groups._index.tsx +++ b/app/routes/rule-groups._index.tsx @@ -1,5 +1,5 @@ import { type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node"; -import { useLoaderData, useSearchParams } from "@remix-run/react"; +import { useLoaderData, useNavigate, useSearchParams } from "@remix-run/react"; import { Fragment, useEffect, useMemo, useRef, useState } from "react"; import axios from "axios"; @@ -15,6 +15,7 @@ import { import { Button } from "~/components/ui/Button"; import { Card } from "~/components/ui/Card"; import { API_BASE_URL } from "~/config/api-config"; +import { usePermission } from "~/hooks/usePermission"; import { parseRuleSummariesFromYaml, type RuleSummary } from "~/utils/rule-yaml-parser"; export function links() { @@ -71,6 +72,32 @@ interface LoaderData { frontendJWT?: string | null; } +interface RuleTemplatePayload { + groupId?: number; + groupName?: string; + parentGroupName?: string; + documentTypeName?: string; + entryModuleName?: string; + ruleType?: string; + ruleName?: string; + yamlTemplate?: string; + yamlText?: string; + ossPreviewKey?: string; +} + +interface RuleDraftCreateResult { + packId?: number | null; + groupId?: number | null; + groupName?: string | null; + ruleSetId?: number | null; + ruleSetName?: string | null; + ruleName?: string | null; + ruleType?: string | null; + versionId?: number | null; + versionNo?: string | null; + bindingId?: number | null; +} + interface GroupFormState { id?: number; mode: "create" | "edit"; @@ -119,22 +146,32 @@ async function fetchGroupTree(token?: string | null): Promise { export async function loader({ request }: LoaderFunctionArgs) { const { getUserSession } = await import("~/api/login/auth.server"); - const { frontendJWT } = await getUserSession(request); + const { frontendJWT, userInfo } = await getUserSession(request); + const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server"); - const [groups, docTypesRes, entryModulesRes, ruleSetsRes] = await Promise.all([ - fetchGroupTree(frontendJWT), - getDocumentTypes({ page: 1, pageSize: 500 }, frontendJWT), - getEntryModules(frontendJWT), - getRuleSets(frontendJWT), - ]); + await requireRoutePermission("/rule-groups", userInfo?.role || "", frontendJWT || undefined); - return Response.json({ - groups, - docTypes: docTypesRes.data?.types || [], - entryModules: entryModulesRes.data || [], - ruleSets: ruleSetsRes.data || [], - frontendJWT, - } satisfies LoaderData); + try { + const [groups, docTypesRes, entryModulesRes, ruleSetsRes] = await Promise.all([ + fetchGroupTree(frontendJWT), + getDocumentTypes({ page: 1, pageSize: 500 }, frontendJWT), + getEntryModules(frontendJWT), + getRuleSets(frontendJWT), + ]); + + return Response.json({ + groups, + docTypes: docTypesRes.data?.types || [], + entryModules: entryModulesRes.data || [], + ruleSets: ruleSetsRes.data || [], + frontendJWT, + } satisfies LoaderData); + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 403) { + throw new Response("无权访问评查点分组页面", { status: 403 }); + } + throw error; + } } function formatVersionLabel(binding: BindingItem): string { @@ -278,6 +315,75 @@ type RulePreviewState = { rules: RuleSummary[]; }; +type RuleDraftFormState = { + groupId: number; + yamlText: string; + changeNote: string; + template: RuleTemplatePayload | null; + loading: boolean; + saving: boolean; + error: string | null; + success: RuleDraftCreateResult | null; +}; + +function unwrapApiData(payload: any): T { + return (payload?.data ?? payload) as T; +} + +function normalizeRuleTemplatePayload(payload: any): RuleTemplatePayload { + const item = unwrapApiData(payload) || {}; + const context = item.context || {}; + return { + groupId: item.groupId ?? item.group_id ?? context.groupId ?? context.group_id, + groupName: item.groupName ?? item.group_name ?? context.groupName ?? context.group_name, + parentGroupName: item.parentGroupName ?? item.root_group_name ?? context.parentGroupName ?? context.parent_group_name, + documentTypeName: item.documentTypeName ?? item.document_type_name ?? context.documentTypeName ?? context.document_type_name, + entryModuleName: item.entryModuleName ?? item.entry_module_name ?? context.entryModuleName ?? context.entry_module_name, + ruleType: item.ruleType ?? item.rule_type, + ruleName: item.ruleName ?? item.rule_name, + yamlTemplate: item.yamlTemplate ?? item.yaml_template, + yamlText: item.yamlText ?? item.yaml_text, + ossPreviewKey: item.ossPreviewKey ?? item.oss_preview_key, + }; +} + +function normalizeRuleDraftCreateResult(payload: any): RuleDraftCreateResult { + const item = unwrapApiData(payload) || {}; + const createdVersion = item.createdVersion ?? item.created_version ?? {}; + const binding = item.binding ?? {}; + return { + packId: item.packId ?? item.pack_id ?? item.groupId ?? item.group_id, + groupId: item.groupId ?? item.group_id, + groupName: item.groupName ?? item.group_name, + ruleSetId: item.ruleSetId ?? item.rule_set_id ?? createdVersion.ruleSetId ?? createdVersion.rule_set_id, + ruleSetName: item.ruleSetName ?? item.rule_set_name ?? binding.rule_name, + ruleName: item.ruleName ?? item.rule_name ?? binding.rule_name, + ruleType: item.ruleType ?? item.rule_type ?? binding.rule_type, + versionId: item.versionId ?? item.version_id ?? createdVersion.id, + versionNo: item.versionNo ?? item.version_no ?? createdVersion.versionNo ?? createdVersion.version_no, + bindingId: item.bindingId ?? item.binding_id ?? binding.id, + }; +} + +function findGroupNode(groups: RuleGroupNode[], groupId: number): RuleGroupNode | null { + for (const root of groups) { + if (root.id === groupId) return root; + const child = (root.children || []).find((item) => item.id === groupId); + if (child) return child; + } + return null; +} + +function buildRuleDraftJumpUrl(group: RuleGroupNode | null, success: RuleDraftCreateResult | null, template: RuleTemplatePayload | null): string { + if (success?.packId) { + return `/rulesTest/detail?packId=${encodeURIComponent(String(success.packId))}`; + } + const params = new URLSearchParams(); + const keyword = success?.ruleType || template?.ruleType || group?.code || group?.name || ""; + if (keyword) params.set("keyword", keyword); + return `/rulesTest/list${params.toString() ? `?${params.toString()}` : ""}`; +} + function GroupModal({ visible, form, @@ -506,10 +612,125 @@ function BindingModal({ ); } +function RuleDraftModal({ + visible, + group, + form, + onClose, + onChange, + onSubmit, + onOpenRules, +}: { + visible: boolean; + group: RuleGroupNode | null; + form: RuleDraftFormState; + onClose: () => void; + onChange: (patch: Partial) => void; + onSubmit: () => void; + onOpenRules: () => void; +}) { + if (!visible || !group) return null; + const template = form.template; + + return ( +
+
+
+

从二级分组新建规则集 / YAML

+ +
+
+
+
+ {getSubtypeGroupDisplayName(group)} + + 所属一级:{template?.parentGroupName || "-"} · 文档类型:{group.document_type_name || template?.documentTypeName || "-"} + {` · 入口模块:${group.entry_module_name || template?.entryModuleName || "-"}`} + +
+
+
+
+ 当前入口只负责生成完整 YAML 草稿 + 模板来自当前二级分组,保存后会创建规则集版本,并尽量同步回当前分组绑定。 +
+
+
+ + +
+
+ + +
+
+ + +

后端正式保存时会按当前版本号写入实际规则文件路径。

+
+
+ + onChange({ changeNote: event.target.value, success: null })} + placeholder="例如:初始化建设工程合同规则草稿" + /> +
+
+ +