fix: tighten route permission guards
This commit is contained in:
+11
-37
@@ -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<string, string[]> = {};
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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,8 +146,12 @@ async function fetchGroupTree(token?: string | null): Promise<RuleGroupNode[]> {
|
||||
|
||||
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("/rule-groups", userInfo?.role || "", frontendJWT || undefined);
|
||||
|
||||
try {
|
||||
const [groups, docTypesRes, entryModulesRes, ruleSetsRes] = await Promise.all([
|
||||
fetchGroupTree(frontendJWT),
|
||||
getDocumentTypes({ page: 1, pageSize: 500 }, frontendJWT),
|
||||
@@ -135,6 +166,12 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
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<T>(payload: any): T {
|
||||
return (payload?.data ?? payload) as T;
|
||||
}
|
||||
|
||||
function normalizeRuleTemplatePayload(payload: any): RuleTemplatePayload {
|
||||
const item = unwrapApiData<any>(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<any>(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<RuleDraftFormState>) => void;
|
||||
onSubmit: () => void;
|
||||
onOpenRules: () => void;
|
||||
}) {
|
||||
if (!visible || !group) return null;
|
||||
const template = form.template;
|
||||
|
||||
return (
|
||||
<div className="rg-modal-backdrop">
|
||||
<div className="rg-modal large">
|
||||
<div className="rg-modal-header">
|
||||
<h3>从二级分组新建规则集 / YAML</h3>
|
||||
<button type="button" className="icon-button" onClick={onClose}>
|
||||
<i className="ri-close-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="rg-modal-body grid-form">
|
||||
<div className="form-item form-item-span-2">
|
||||
<div className="modal-tip">
|
||||
<strong>{getSubtypeGroupDisplayName(group)}</strong>
|
||||
<span>
|
||||
所属一级:{template?.parentGroupName || "-"} · 文档类型:{group.document_type_name || template?.documentTypeName || "-"}
|
||||
{` · 入口模块:${group.entry_module_name || template?.entryModuleName || "-"}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-item form-item-span-2">
|
||||
<div className="detail-alert info compact">
|
||||
<strong>当前入口只负责生成完整 YAML 草稿</strong>
|
||||
<span>模板来自当前二级分组,保存后会创建规则集版本,并尽量同步回当前分组绑定。</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<label>规则类型</label>
|
||||
<input value={template?.ruleType || ""} readOnly placeholder={form.loading ? "模板加载中..." : "等待后端返回"} />
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<label>规则集名称</label>
|
||||
<input value={template?.ruleName || ""} readOnly placeholder={form.loading ? "模板加载中..." : "等待后端返回"} />
|
||||
</div>
|
||||
<div className="form-item form-item-span-2">
|
||||
<label>OSS 预览路径</label>
|
||||
<input value={template?.ossPreviewKey || ""} readOnly placeholder={form.loading ? "模板加载中..." : "等待后端返回"} />
|
||||
<p className="field-tip">后端正式保存时会按当前版本号写入实际规则文件路径。</p>
|
||||
</div>
|
||||
<div className="form-item form-item-span-2">
|
||||
<label>变更说明</label>
|
||||
<input
|
||||
value={form.changeNote}
|
||||
onChange={(event) => onChange({ changeNote: event.target.value, success: null })}
|
||||
placeholder="例如:初始化建设工程合同规则草稿"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item form-item-span-2">
|
||||
<label>完整 YAML</label>
|
||||
<textarea
|
||||
rows={24}
|
||||
value={form.yamlText}
|
||||
onChange={(event) => onChange({ yamlText: event.target.value, error: null, success: null })}
|
||||
placeholder={form.loading ? "模板加载中..." : "这里显示后端生成的完整 YAML 模板"}
|
||||
/>
|
||||
<p className="field-tip">这里直接提交完整 YAML 文本,不再由前端重组规则结构。</p>
|
||||
</div>
|
||||
{form.error ? (
|
||||
<div className="form-item form-item-span-2">
|
||||
<div className="detail-alert danger compact">
|
||||
<strong>保存失败</strong>
|
||||
<span>{form.error}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{form.success ? (
|
||||
<div className="form-item form-item-span-2">
|
||||
<div className="detail-alert success compact">
|
||||
<strong>规则草稿已保存</strong>
|
||||
<span>
|
||||
{form.success.ruleName || form.success.ruleSetName || template?.ruleName || "规则集"} ·
|
||||
{` 版本 ${form.success.versionNo || form.success.versionId || "已创建"} `}
|
||||
{form.success.bindingId ? `· 已同步绑定 #${form.success.bindingId}` : "· 绑定结果以后端返回为准"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="rg-modal-footer">
|
||||
<Button type="default" onClick={onClose}>关闭</Button>
|
||||
{form.success ? (
|
||||
<Button type="primary" onClick={onOpenRules}>继续进入规则页</Button>
|
||||
) : (
|
||||
<Button type="primary" onClick={onSubmit} disabled={form.loading || form.saving}>
|
||||
{form.loading ? "模板加载中..." : form.saving ? "保存中..." : "保存规则草稿"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RuleGroupsIndex() {
|
||||
const { groups, docTypes, entryModules, ruleSets, frontendJWT } = useLoaderData<LoaderData>();
|
||||
const navigate = useNavigate();
|
||||
const { hasAnyPermission } = usePermission();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const topGroups = useMemo(() => toTopGroups(groups), [groups]);
|
||||
const [groupTree, setGroupTree] = useState<RuleGroupNode[]>(groups);
|
||||
const topGroups = useMemo(() => toTopGroups(groupTree), [groupTree]);
|
||||
const nameValue = searchParams.get("name") || "";
|
||||
const codeValue = searchParams.get("code") || "";
|
||||
const statusValue = searchParams.get("status") || "";
|
||||
@@ -521,6 +742,7 @@ export default function RuleGroupsIndex() {
|
||||
const [rulePreviewMap, setRulePreviewMap] = useState<Record<number, RulePreviewState>>({});
|
||||
const [groupModalOpen, setGroupModalOpen] = useState(false);
|
||||
const [bindingModalOpen, setBindingModalOpen] = useState(false);
|
||||
const [draftModalOpen, setDraftModalOpen] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [groupForm, setGroupForm] = useState<GroupFormState>({
|
||||
mode: "create",
|
||||
@@ -542,8 +764,22 @@ export default function RuleGroupsIndex() {
|
||||
isActive: true,
|
||||
});
|
||||
const [activeBindingGroup, setActiveBindingGroup] = useState<RuleGroupNode | null>(null);
|
||||
const [activeDraftGroup, setActiveDraftGroup] = useState<RuleGroupNode | null>(null);
|
||||
const [draftForm, setDraftForm] = useState<RuleDraftFormState>({
|
||||
groupId: 0,
|
||||
yamlText: "",
|
||||
changeNote: "",
|
||||
template: null,
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
success: null,
|
||||
});
|
||||
const mountedRef = useRef(false);
|
||||
const childGroupStats = useMemo(() => buildChildGroupStats(topGroups), [topGroups]);
|
||||
const canManageGroups = hasAnyPermission(["evaluation_group:create:write", "evaluation_group:update:write"]);
|
||||
const canManageBindings = hasAnyPermission(["evaluation_group:update:write"]);
|
||||
const canCreateRuleDraft = hasAnyPermission(["evaluation_group:update:write", "rules:create:write"]);
|
||||
|
||||
const filteredGroups = useMemo(() => {
|
||||
return topGroups
|
||||
@@ -605,8 +841,12 @@ export default function RuleGroupsIndex() {
|
||||
|
||||
const resetFilters = () => setSearchParams(new URLSearchParams());
|
||||
|
||||
const reloadPage = () => {
|
||||
if (typeof window !== "undefined") window.location.reload();
|
||||
const reloadPage = async (): Promise<RuleGroupNode[]> => {
|
||||
const nextGroups = await fetchGroupTree(frontendJWT);
|
||||
setGroupTree(nextGroups);
|
||||
setActiveBindingGroup((current) => (current ? findGroupNode(nextGroups, current.id) : null));
|
||||
setActiveDraftGroup((current) => (current ? findGroupNode(nextGroups, current.id) : null));
|
||||
return nextGroups;
|
||||
};
|
||||
|
||||
const handleApiError = (error: any) => {
|
||||
@@ -656,6 +896,39 @@ export default function RuleGroupsIndex() {
|
||||
setBindingModalOpen(true);
|
||||
};
|
||||
|
||||
const openCreateRuleDraft = async (group: RuleGroupNode) => {
|
||||
setActiveDraftGroup(group);
|
||||
setDraftForm({
|
||||
groupId: group.id,
|
||||
yamlText: "",
|
||||
changeNote: "",
|
||||
template: null,
|
||||
loading: true,
|
||||
saving: false,
|
||||
error: null,
|
||||
success: null,
|
||||
});
|
||||
setDraftModalOpen(true);
|
||||
try {
|
||||
const response = await axios.get(`${API_BASE_URL}/api/v3/evaluation-point-groups/${group.id}/rule-template`, {
|
||||
headers: authHeaders(frontendJWT),
|
||||
});
|
||||
const template = normalizeRuleTemplatePayload(response?.data);
|
||||
const yamlText = String(template?.yamlTemplate || template?.yamlText || "");
|
||||
setDraftForm((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
template,
|
||||
yamlText,
|
||||
changeNote: prev.changeNote || `初始化 ${getSubtypeGroupDisplayName(group)} 规则草稿`,
|
||||
error: yamlText.trim() ? null : "后端未返回可编辑的 YAML 模板",
|
||||
}));
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.msg || error?.response?.data?.detail || error?.message || "模板加载失败";
|
||||
setDraftForm((prev) => ({ ...prev, loading: false, error: String(message) }));
|
||||
}
|
||||
};
|
||||
|
||||
const openEditBinding = (group: RuleGroupNode, binding: BindingItem) => {
|
||||
setActiveBindingGroup(group);
|
||||
setBindingForm({
|
||||
@@ -691,7 +964,8 @@ export default function RuleGroupsIndex() {
|
||||
} else {
|
||||
await axios.put(`${API_BASE_URL}/api/v3/evaluation-point-groups/${groupForm.id}`, payload, { headers: { ...authHeaders(frontendJWT) } });
|
||||
}
|
||||
reloadPage();
|
||||
await reloadPage();
|
||||
setGroupModalOpen(false);
|
||||
} catch (error) {
|
||||
handleApiError(error);
|
||||
} finally {
|
||||
@@ -714,7 +988,8 @@ export default function RuleGroupsIndex() {
|
||||
} else {
|
||||
await axios.put(`${API_BASE_URL}/api/v3/evaluation-point-groups/bindings/${bindingForm.bindingId}`, payload, { headers: { ...authHeaders(frontendJWT) } });
|
||||
}
|
||||
reloadPage();
|
||||
await reloadPage();
|
||||
setBindingModalOpen(false);
|
||||
} catch (error) {
|
||||
handleApiError(error);
|
||||
} finally {
|
||||
@@ -729,7 +1004,7 @@ export default function RuleGroupsIndex() {
|
||||
const response = await axios.delete(`${API_BASE_URL}/api/v3/evaluation-point-groups/${group.id}`, { headers: authHeaders(frontendJWT) });
|
||||
const payload = response?.data?.data ?? response?.data;
|
||||
if (payload?.success === false) return window.alert(payload?.message || "删除失败");
|
||||
reloadPage();
|
||||
await reloadPage();
|
||||
} catch (error) {
|
||||
handleApiError(error);
|
||||
}
|
||||
@@ -739,12 +1014,45 @@ export default function RuleGroupsIndex() {
|
||||
if (!window.confirm(`确认解除规则集「${binding.rule_name || binding.rule_type || binding.rule_set_id}」吗?`)) return;
|
||||
try {
|
||||
await axios.delete(`${API_BASE_URL}/api/v3/evaluation-point-groups/bindings/${binding.id}`, { headers: authHeaders(frontendJWT) });
|
||||
reloadPage();
|
||||
await reloadPage();
|
||||
} catch (error) {
|
||||
handleApiError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const submitRuleDraft = async () => {
|
||||
if (!activeDraftGroup) return;
|
||||
if (!draftForm.yamlText.trim()) {
|
||||
setDraftForm((prev) => ({ ...prev, error: "请先确认 YAML 内容后再保存" }));
|
||||
return;
|
||||
}
|
||||
setDraftForm((prev) => ({ ...prev, saving: true, error: null }));
|
||||
try {
|
||||
const payload = {
|
||||
yaml_text: draftForm.yamlText,
|
||||
change_note: draftForm.changeNote.trim() || null,
|
||||
};
|
||||
const response = await axios.post(
|
||||
`${API_BASE_URL}/api/v3/evaluation-point-groups/${activeDraftGroup.id}/rule-drafts`,
|
||||
payload,
|
||||
{ headers: authHeaders(frontendJWT) },
|
||||
);
|
||||
const result = normalizeRuleDraftCreateResult(response?.data);
|
||||
const nextGroups = await reloadPage();
|
||||
const refreshedGroup = findGroupNode(nextGroups, activeDraftGroup.id) || activeDraftGroup;
|
||||
setActiveDraftGroup(refreshedGroup);
|
||||
setDraftForm((prev) => ({ ...prev, saving: false, success: result }));
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.msg || error?.response?.data?.detail || error?.message || "规则草稿保存失败";
|
||||
setDraftForm((prev) => ({ ...prev, saving: false, error: String(message) }));
|
||||
}
|
||||
};
|
||||
|
||||
const openRuleDraftResult = () => {
|
||||
const target = buildRuleDraftJumpUrl(activeDraftGroup, draftForm.success, draftForm.template);
|
||||
navigate(target);
|
||||
};
|
||||
|
||||
const toggleRoot = (groupId: number) => {
|
||||
setExpandedGroupIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -812,7 +1120,9 @@ export default function RuleGroupsIndex() {
|
||||
<div className="page-actions">
|
||||
<Button type="default" icon="ri-expand-up-down-line" onClick={() => setExpandedGroupIds(new Set(filteredGroups.map((item) => item.id)))}>展开全部</Button>
|
||||
<Button type="default" icon="ri-collapse-diagonal-line" onClick={() => { setExpandedGroupIds(new Set()); setExpandedBindingIds(new Set()); }}>收起全部</Button>
|
||||
{canManageGroups ? (
|
||||
<Button type="primary" icon="ri-add-line" onClick={openCreateRoot}>新增一级分组</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -959,9 +1269,15 @@ export default function RuleGroupsIndex() {
|
||||
<td>{formatDateTime(root.updated_at || root.created_at)}</td>
|
||||
<td>
|
||||
<div className="action-links">
|
||||
{canManageGroups ? (
|
||||
<button type="button" className="action-link button-link" onClick={() => openCreateChild(root)}>新增二级分组</button>
|
||||
) : null}
|
||||
{canManageGroups ? (
|
||||
<button type="button" className="action-link button-link" onClick={() => openEditGroup(root)}>编辑</button>
|
||||
) : null}
|
||||
{canManageGroups ? (
|
||||
<button type="button" className="action-link danger button-link" onClick={() => deleteGroup(root)}>删除</button>
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1026,9 +1342,18 @@ export default function RuleGroupsIndex() {
|
||||
>
|
||||
{child.bindings.length ? (child.bindings.every((item) => expandedBindingIds.has(item.id)) ? "收起规则集" : "查看规则集") : "暂无规则集"}
|
||||
</button>
|
||||
{canCreateRuleDraft ? (
|
||||
<button type="button" className="action-link button-link" onClick={() => openCreateRuleDraft(child)}>新建规则集/YAML</button>
|
||||
) : null}
|
||||
{canManageBindings ? (
|
||||
<button type="button" className="action-link button-link" onClick={() => openCreateBinding(child)}>绑定规则集</button>
|
||||
) : null}
|
||||
{canManageGroups ? (
|
||||
<button type="button" className="action-link button-link" onClick={() => openEditGroup(child)}>编辑</button>
|
||||
) : null}
|
||||
{canManageGroups ? (
|
||||
<button type="button" className="action-link danger button-link" onClick={() => deleteGroup(child)}>删除</button>
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1071,8 +1396,12 @@ export default function RuleGroupsIndex() {
|
||||
<button type="button" className="action-link button-link" onClick={() => toggleBindingPreview(binding)}>
|
||||
{expandedBinding ? "收起规则明细" : "查看组内规则"}
|
||||
</button>
|
||||
{canManageBindings ? (
|
||||
<button type="button" className="action-link button-link" onClick={() => openEditBinding(child, binding)}>编辑绑定</button>
|
||||
) : null}
|
||||
{canManageBindings ? (
|
||||
<button type="button" className="action-link danger button-link" onClick={() => deleteBinding(binding)}>解除绑定</button>
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1157,6 +1486,15 @@ export default function RuleGroupsIndex() {
|
||||
onSubmit={submitBinding}
|
||||
saving={saving}
|
||||
/>
|
||||
<RuleDraftModal
|
||||
visible={draftModalOpen}
|
||||
group={activeDraftGroup}
|
||||
form={draftForm}
|
||||
onClose={() => setDraftModalOpen(false)}
|
||||
onChange={(patch) => setDraftForm((prev) => ({ ...prev, ...patch }))}
|
||||
onSubmit={submitRuleDraft}
|
||||
onOpenRules={openRuleDraftResult}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,6 +237,10 @@ function mapApiRuleToModel(apiRule: ApiRule): Rule {
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT, userInfo } = await getUserSession(request);
|
||||
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server");
|
||||
await requireRoutePermission("/rules/list", userInfo?.role || "", frontendJWT || undefined);
|
||||
|
||||
// 从 URL 参数中提取查询条件
|
||||
const params = {
|
||||
@@ -280,6 +284,10 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
export async function action({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT, userInfo } = await getUserSession(request);
|
||||
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server");
|
||||
await requireRoutePermission("/rules/list", userInfo?.role || "", frontendJWT || undefined);
|
||||
const formData = await request.formData();
|
||||
const _action = formData.get('_action');
|
||||
const ruleId = formData.get('ruleId');
|
||||
|
||||
@@ -18,6 +18,11 @@ export const handle = {
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT, userInfo } = await getUserSession(request);
|
||||
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server");
|
||||
|
||||
await requireRoutePermission("/rules", userInfo?.role || "", frontendJWT || undefined);
|
||||
|
||||
if (url.pathname === '/rules') {
|
||||
const query = url.searchParams.toString();
|
||||
|
||||
@@ -314,6 +314,9 @@ function validateRule(rule: RuleSummary | undefined, dependencyOptions: Dependen
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const { frontendJWT, userInfo } = await getUserSession(request);
|
||||
const { requireRoutePermission } = await import('~/api/auth/check-route-permission.server');
|
||||
await requireRoutePermission('/rulesTest/detail', userInfo?.role || '', frontendJWT || undefined);
|
||||
const packId = url.searchParams.get('packId') || url.searchParams.get('id') || '';
|
||||
const requestedRuleId = url.searchParams.get('ruleId') || '';
|
||||
const packs = await loadRuleConfigPacks(request);
|
||||
@@ -330,6 +333,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const { frontendJWT, userInfo } = await getUserSession(request);
|
||||
const { requireRoutePermission } = await import('~/api/auth/check-route-permission.server');
|
||||
await requireRoutePermission('/rulesTest/detail', userInfo?.role || '', frontendJWT || undefined);
|
||||
if (!frontendJWT) {
|
||||
return json<ActionData>({ success: false, intent: 'save', message: '登录已失效,请重新登录后再保存。' }, { status: 401 });
|
||||
}
|
||||
|
||||
@@ -84,6 +84,10 @@ function riskColor(risk: string): TagColor {
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT, userInfo } = await getUserSession(request);
|
||||
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server");
|
||||
await requireRoutePermission("/rulesTest/list", userInfo?.role || "", frontendJWT || undefined);
|
||||
const requestedMainType = url.searchParams.get('mainType') || url.searchParams.get('ruleTypeName') || '';
|
||||
const requestedSubtype = url.searchParams.get('subtype') || url.searchParams.get('documentAttributeType') || '';
|
||||
const requestedRuleGroup = url.searchParams.get('ruleGroup') || url.searchParams.getAll('ruleGroups')[0] || '';
|
||||
|
||||
Reference in New Issue
Block a user