feat: sync rule management and review ui fixes
This commit is contained in:
+14
-10
@@ -219,10 +219,12 @@ function getFileExtension(filename: string): string {
|
|||||||
|
|
||||||
function mapProcessingStatusToFileStatus(status?: string | null): string {
|
function mapProcessingStatusToFileStatus(status?: string | null): string {
|
||||||
const normalized = (status || '').toLowerCase();
|
const normalized = (status || '').toLowerCase();
|
||||||
if (normalized === 'completed') return 'Processed';
|
if (normalized === 'completed' || normalized === 'processed') return 'Processed';
|
||||||
if (normalized === 'failed') return 'Failed';
|
if (normalized === 'failed') return 'Failed';
|
||||||
if (normalized === 'running' || normalized === 'queued' || normalized === 'dispatch') return 'Evaluationing';
|
if (normalized === 'cutting') return 'Cutting';
|
||||||
if (normalized === 'waiting' || normalized === 'pending') return 'Waiting';
|
if (normalized === 'extractioning') return 'Extractioning';
|
||||||
|
if (normalized === 'evaluationing' || normalized === 'running' || normalized === 'dispatch') return 'Evaluationing';
|
||||||
|
if (normalized === 'waiting' || normalized === 'pending' || normalized === 'queued') return 'Waiting';
|
||||||
return 'Waiting';
|
return 'Waiting';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -827,13 +829,15 @@ export async function getDocumentsListFromAPI(searchParams: {
|
|||||||
if (name) params.keyword = name;
|
if (name) params.keyword = name;
|
||||||
if (fileStatus) {
|
if (fileStatus) {
|
||||||
const normalizedFileStatus = fileStatus.toLowerCase();
|
const normalizedFileStatus = fileStatus.toLowerCase();
|
||||||
if (normalizedFileStatus === 'processed') {
|
const processingStatusMap: Record<string, string> = {
|
||||||
params.processingStatus = 'completed';
|
waiting: 'waiting',
|
||||||
} else if (normalizedFileStatus === 'failed') {
|
cutting: 'Cutting',
|
||||||
params.processingStatus = 'failed';
|
extractioning: 'Extractioning',
|
||||||
} else {
|
evaluationing: 'Evaluationing',
|
||||||
params.processingStatus = 'running';
|
processed: 'completed',
|
||||||
}
|
failed: 'failed',
|
||||||
|
};
|
||||||
|
params.processingStatus = processingStatusMap[normalizedFileStatus] || fileStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (documentTypeIds && documentTypeIds.length > 0) {
|
if (documentTypeIds && documentTypeIds.length > 0) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import type { MenuItem } from '~/api/auth/user-routes';
|
||||||
import { Sidebar } from './Sidebar';
|
import { Sidebar } from './Sidebar';
|
||||||
// import { Header } from './Header';
|
// import { Header } from './Header';
|
||||||
import { Breadcrumb } from './Breadcrumb';
|
import { Breadcrumb } from './Breadcrumb';
|
||||||
@@ -10,6 +11,7 @@ interface LayoutProps {
|
|||||||
userRole?: UserRole;
|
userRole?: UserRole;
|
||||||
frontendJWT?: string;
|
frontendJWT?: string;
|
||||||
isMobile?: boolean; // 是否为移动端设备(服务端通过 User-Agent 检测)
|
isMobile?: boolean; // 是否为移动端设备(服务端通过 User-Agent 检测)
|
||||||
|
menuItems?: MenuItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加一个接口表示路由handle可能包含的属性
|
// 添加一个接口表示路由handle可能包含的属性
|
||||||
@@ -37,7 +39,7 @@ type RulesTestDetailData = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Layout({ children, userRole = 'developer' as UserRole, frontendJWT = '', isMobile = false }: LayoutProps) {
|
export function Layout({ children, userRole = 'developer' as UserRole, frontendJWT = '', isMobile = false, menuItems = [] }: LayoutProps) {
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
const [effectiveUserRole, setEffectiveUserRole] = useState<UserRole>(userRole);
|
const [effectiveUserRole, setEffectiveUserRole] = useState<UserRole>(userRole);
|
||||||
const [effectiveFrontendJWT, setEffectiveFrontendJWT] = useState<string>(frontendJWT);
|
const [effectiveFrontendJWT, setEffectiveFrontendJWT] = useState<string>(frontendJWT);
|
||||||
@@ -153,6 +155,7 @@ export function Layout({ children, userRole = 'developer' as UserRole, frontendJ
|
|||||||
onToggle={toggleSidebar}
|
onToggle={toggleSidebar}
|
||||||
userRole={effectiveUserRole}
|
userRole={effectiveUserRole}
|
||||||
frontendJWT={effectiveFrontendJWT}
|
frontendJWT={effectiveFrontendJWT}
|
||||||
|
menuItems={menuItems}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 规则详情页顶部栏 */}
|
{/* 规则详情页顶部栏 */}
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ interface SidebarProps {
|
|||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
userRole: UserRole;
|
userRole: UserRole;
|
||||||
frontendJWT?: string;
|
frontendJWT?: string;
|
||||||
|
menuItems?: MenuItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: SidebarProps) {
|
export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '', menuItems: initialMenuItems = [] }: SidebarProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [expandedMenus, setExpandedMenus] = useState<Record<string, boolean>>({});
|
const [expandedMenus, setExpandedMenus] = useState<Record<string, boolean>>({});
|
||||||
const [menuItems, setMenuItems] = useState<MenuItem[]>([]); // 动态菜单项
|
const [menuItems, setMenuItems] = useState<MenuItem[]>(initialMenuItems); // 动态菜单项
|
||||||
const [isLoadingRoutes, setIsLoadingRoutes] = useState<boolean>(true); // 路由加载状态
|
const [isLoadingRoutes, setIsLoadingRoutes] = useState<boolean>(initialMenuItems.length === 0); // 路由加载状态
|
||||||
const [isMobile, setIsMobile] = useState<boolean>(false); // 移动端检测
|
const [isMobile, setIsMobile] = useState<boolean>(false); // 移动端检测
|
||||||
const [selectedModuleName, setSelectedModuleName] = useState<string>(''); // 当前选中的模块名称
|
const [selectedModuleName, setSelectedModuleName] = useState<string>(''); // 当前选中的模块名称
|
||||||
const [selectedModulePicPath, setSelectedModulePicPath] = useState<string>(''); // 当前选中的模块图片路径
|
const [selectedModulePicPath, setSelectedModulePicPath] = useState<string>(''); // 当前选中的模块图片路径
|
||||||
@@ -39,12 +40,15 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
|||||||
|
|
||||||
// 获取用户路由权限
|
// 获取用户路由权限
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// console.log('🔍 [Sidebar] useEffect 触发,开始获取路由权限');
|
if (initialMenuItems.length > 0) {
|
||||||
|
setMenuItems(initialMenuItems);
|
||||||
|
setIsLoadingRoutes(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const fetchUserRoutes = async () => {
|
const fetchUserRoutes = async () => {
|
||||||
setIsLoadingRoutes(true);
|
setIsLoadingRoutes(true);
|
||||||
try {
|
try {
|
||||||
// 优先使用传入的 frontendJWT,否则从 localStorage 读取
|
|
||||||
let jwt = frontendJWT;
|
let jwt = frontendJWT;
|
||||||
|
|
||||||
if (!jwt && typeof window !== 'undefined') {
|
if (!jwt && typeof window !== 'undefined') {
|
||||||
@@ -59,29 +63,20 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('🔍 [Sidebar] 当前用户角色:', userRole, 'JWT前20字符:', jwt.substring(0, 20));
|
|
||||||
// console.log('🔍 [Sidebar] 映射后的角色key:', roleKey);
|
|
||||||
const result = await getUserRoutesByRole(userRole, jwt);
|
const result = await getUserRoutesByRole(userRole, jwt);
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
setMenuItems(result.data);
|
setMenuItems(result.data);
|
||||||
// console.log('✅ [Sidebar] 用户路由权限加载成功:', result.data);
|
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ [Sidebar] 获取用户路由权限失败:', result.error);
|
console.error('❌ [Sidebar] 获取用户路由权限失败:', result.error);
|
||||||
|
|
||||||
// 如果需要重定向到首页
|
|
||||||
if (result.shouldRedirectToHome) {
|
if (result.shouldRedirectToHome) {
|
||||||
// console.log('🔄 [Sidebar] 重定向到首页');
|
|
||||||
navigate('/');
|
navigate('/');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 其他错误情况,使用空数组
|
|
||||||
setMenuItems([]);
|
setMenuItems([]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ [Sidebar] 获取用户路由权限时发生错误:', error);
|
console.error('❌ [Sidebar] 获取用户路由权限时发生错误:', error);
|
||||||
// 发生异常时也重定向到首页
|
|
||||||
navigate('/');
|
navigate('/');
|
||||||
return;
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -90,7 +85,7 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchUserRoutes();
|
fetchUserRoutes();
|
||||||
}, [userRole, frontendJWT, navigate]);
|
}, [userRole, frontendJWT, navigate, initialMenuItems]);
|
||||||
|
|
||||||
// 🔑 检查是否处于系统设置模式或交叉评查模式
|
// 🔑 检查是否处于系统设置模式或交叉评查模式
|
||||||
const [isSettingsMode, setIsSettingsMode] = useState<boolean>(false);
|
const [isSettingsMode, setIsSettingsMode] = useState<boolean>(false);
|
||||||
|
|||||||
@@ -27,12 +27,13 @@ interface ReviewTabsProps {
|
|||||||
comparisonId?: number;
|
comparisonId?: number;
|
||||||
};
|
};
|
||||||
onConfirmResults: () => void;
|
onConfirmResults: () => void;
|
||||||
|
onExportReport?: () => void;
|
||||||
jwtToken?: string | null;
|
jwtToken?: string | null;
|
||||||
/** 下载前保存文档的回调,返回 true 表示保存成功可以继续下载 */
|
/** 下载前保存文档的回调,返回 true 表示保存成功可以继续下载 */
|
||||||
onSaveBeforeDownload?: () => Promise<boolean>;
|
onSaveBeforeDownload?: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfirmResults, jwtToken, onSaveBeforeDownload }: ReviewTabsProps) {
|
export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfirmResults, onExportReport, jwtToken, onSaveBeforeDownload }: ReviewTabsProps) {
|
||||||
const [isNavigating, setIsNavigating] = useState(false);
|
const [isNavigating, setIsNavigating] = useState(false);
|
||||||
const [isReuploadModalOpen, setIsReuploadModalOpen] = useState(false);
|
const [isReuploadModalOpen, setIsReuploadModalOpen] = useState(false);
|
||||||
const [selectedTemplateFiles, setSelectedTemplateFiles] = useState<File[]>([]);
|
const [selectedTemplateFiles, setSelectedTemplateFiles] = useState<File[]>([]);
|
||||||
@@ -58,14 +59,21 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
|||||||
: previousRoute === 'filesUpload'
|
: previousRoute === 'filesUpload'
|
||||||
? "/files/upload"
|
? "/files/upload"
|
||||||
: "/rules-files";
|
: "/rules-files";
|
||||||
// 立即导航返回
|
navigate(returnTo);
|
||||||
navigate(returnTo);
|
setTimeout(() => {
|
||||||
// 触发上级页面数据重新加载
|
|
||||||
revalidator.revalidate();
|
revalidator.revalidate();
|
||||||
|
setIsNavigating(false);
|
||||||
|
loadingBarService.hide();
|
||||||
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 下载原文件
|
// 下载原文件
|
||||||
const handleDownloadFile = async () => {
|
const handleDownloadFile = async () => {
|
||||||
|
if (!fileInfo.path) {
|
||||||
|
toastService.warning('当前文档暂无可下载原文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 如果有保存回调,先执行保存(仅对 DOCX 文件有效)
|
// 如果有保存回调,先执行保存(仅对 DOCX 文件有效)
|
||||||
if (onSaveBeforeDownload) {
|
if (onSaveBeforeDownload) {
|
||||||
@@ -311,12 +319,15 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{/* <button
|
{onExportReport && (
|
||||||
className="ant-btn ant-btn-default flex items-center"
|
<button
|
||||||
onClick={handleExportReport}
|
className="ant-btn ant-btn-default inline-flex items-center my-2"
|
||||||
>
|
onClick={onExportReport}
|
||||||
<i className="ri-file-copy-line mr-1"></i> 导出评查报告
|
disabled={isNavigating}
|
||||||
</button> */}
|
>
|
||||||
|
<i className="ri-file-copy-line mr-1"></i> 导出评查报告
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
className={`ant-btn ant-btn-primary my-2 flex items-center ${fileInfo.auditStatus === 1 ? 'hidden' : ''}`}
|
className={`ant-btn ant-btn-primary my-2 flex items-center ${fileInfo.auditStatus === 1 ? 'hidden' : ''}`}
|
||||||
onClick={onConfirmResults}
|
onClick={onConfirmResults}
|
||||||
|
|||||||
+18
-2
@@ -69,9 +69,21 @@ export type { UserRole };
|
|||||||
// 辅助函数:从 MenuItem 数组中提取所有路径(包括子路由)
|
// 辅助函数:从 MenuItem 数组中提取所有路径(包括子路由)
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
path: string;
|
path: string;
|
||||||
|
title?: string;
|
||||||
|
hideBreadcrumb?: boolean;
|
||||||
children?: MenuItem[];
|
children?: MenuItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function filterVisibleMenuItems(menuItems: MenuItem[]): MenuItem[] {
|
||||||
|
return menuItems
|
||||||
|
.filter((item) => !item.hideBreadcrumb)
|
||||||
|
.map((item) => ({
|
||||||
|
...item,
|
||||||
|
children: item.children ? filterVisibleMenuItems(item.children) : undefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
function extractAllPaths(menuItems: MenuItem[]): string[] {
|
function extractAllPaths(menuItems: MenuItem[]): string[] {
|
||||||
const paths: string[] = [];
|
const paths: string[] = [];
|
||||||
|
|
||||||
@@ -208,6 +220,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
let userInfo: any = null;
|
let userInfo: any = null;
|
||||||
let allowedPaths: string[] = []; // 用户允许访问的路由列表
|
let allowedPaths: string[] = []; // 用户允许访问的路由列表
|
||||||
let permissionMap: Record<string, string[]> = {}; // ✅ 权限映射表
|
let permissionMap: Record<string, string[]> = {}; // ✅ 权限映射表
|
||||||
|
let menuItems: MenuItem[] = [];
|
||||||
|
|
||||||
if (!isPublicPath) {
|
if (!isPublicPath) {
|
||||||
try {
|
try {
|
||||||
@@ -252,6 +265,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
allowedPaths = extractAllPaths(routesResult.data);
|
allowedPaths = extractAllPaths(routesResult.data);
|
||||||
// console.log("🔑 [Root Loader] 用户允许的路由:", allowedPaths);
|
// console.log("🔑 [Root Loader] 用户允许的路由:", allowedPaths);
|
||||||
|
|
||||||
|
menuItems = filterVisibleMenuItems(routesResult.data as MenuItem[]);
|
||||||
|
|
||||||
// ✅ 保存权限映射表
|
// ✅ 保存权限映射表
|
||||||
if (routesResult.permissionMap) {
|
if (routesResult.permissionMap) {
|
||||||
permissionMap = routesResult.permissionMap;
|
permissionMap = routesResult.permissionMap;
|
||||||
@@ -366,6 +381,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
isPublicPath, // 传递给客户端,用于判断是否需要认证
|
isPublicPath, // 传递给客户端,用于判断是否需要认证
|
||||||
isMobile, // 🔒 传递移动端标识
|
isMobile, // 🔒 传递移动端标识
|
||||||
permissionMap, // ✅ 传递权限映射表
|
permissionMap, // ✅ 传递权限映射表
|
||||||
|
menuItems,
|
||||||
ENV: {
|
ENV: {
|
||||||
// 客户端不再需要直接调用 Dify API
|
// 客户端不再需要直接调用 Dify API
|
||||||
},
|
},
|
||||||
@@ -401,7 +417,7 @@ export function links() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { userRole, ENV, frontendJWT, userInfo, isPublicPath, isMobile } = useLoaderData<typeof loader>();
|
const { userRole, ENV, frontendJWT, userInfo, isPublicPath, isMobile, menuItems } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -441,7 +457,7 @@ export default function App() {
|
|||||||
{isPublicPath ? (
|
{isPublicPath ? (
|
||||||
<Outlet />
|
<Outlet />
|
||||||
) : (
|
) : (
|
||||||
<Layout userRole={userRole} frontendJWT={frontendJWT} isMobile={isMobile}>
|
<Layout userRole={userRole} frontendJWT={frontendJWT} isMobile={isMobile} menuItems={menuItems}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Layout>
|
</Layout>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ interface DocumentListScope {
|
|||||||
documentTypeIds: number[];
|
documentTypeIds: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LIST_SCOPE_STORAGE_KEY = 'documents.listScope';
|
||||||
|
|
||||||
// 审核状态筛选选项
|
// 审核状态筛选选项
|
||||||
const auditStatusOptions = [
|
const auditStatusOptions = [
|
||||||
// { value: "", label: "全部" },
|
// { value: "", label: "全部" },
|
||||||
@@ -257,6 +259,10 @@ export default function DocumentsIndex() {
|
|||||||
sessionStorage.setItem(SEARCH_PARAMS_STORAGE_KEY, params.toString());
|
sessionStorage.setItem(SEARCH_PARAMS_STORAGE_KEY, params.toString());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const persistListScope = useCallback((scope: DocumentListScope) => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
localStorage.setItem(LIST_SCOPE_STORAGE_KEY, JSON.stringify(scope));
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 首次进入且 URL 无任何查询参数时,尝试从 sessionStorage 恢复
|
// 首次进入且 URL 无任何查询参数时,尝试从 sessionStorage 恢复
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -311,7 +317,38 @@ export default function DocumentsIndex() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { selectedModuleId, documentTypeIds };
|
if (selectedModuleId || documentTypeIds.length > 0) {
|
||||||
|
const scope = { selectedModuleId, documentTypeIds };
|
||||||
|
persistListScope(scope);
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedScope = localStorage.getItem(LIST_SCOPE_STORAGE_KEY);
|
||||||
|
if (storedScope) {
|
||||||
|
try {
|
||||||
|
const parsedScope = JSON.parse(storedScope) as Partial<DocumentListScope>;
|
||||||
|
return {
|
||||||
|
selectedModuleId: Number.isFinite(Number(parsedScope.selectedModuleId)) && Number(parsedScope.selectedModuleId) > 0
|
||||||
|
? Number(parsedScope.selectedModuleId)
|
||||||
|
: null,
|
||||||
|
documentTypeIds: Array.isArray(parsedScope.documentTypeIds)
|
||||||
|
? parsedScope.documentTypeIds.map((item) => Number(item)).filter((item) => Number.isFinite(item) && item > 0)
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析 localStorage 文档列表作用域失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { selectedModuleId: null, documentTypeIds: [] };
|
||||||
|
}, [persistListScope]);
|
||||||
|
|
||||||
|
const isDeletableFileStatus = useCallback((status?: string | null) => {
|
||||||
|
return status === 'Processed' || status === 'Failed';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hasHistoryResultStats = useCallback((doc: DocumentVersionUI) => {
|
||||||
|
return [doc.pass_count, doc.warning_count, doc.error_count, doc.manual_count].some((value) => value !== null && value !== undefined);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 客户端数据请求
|
// 客户端数据请求
|
||||||
@@ -412,6 +449,7 @@ export default function DocumentsIndex() {
|
|||||||
try {
|
try {
|
||||||
const nextScope = readListScopeFromSession();
|
const nextScope = readListScopeFromSession();
|
||||||
setListScope(nextScope);
|
setListScope(nextScope);
|
||||||
|
persistListScope(nextScope);
|
||||||
setScopeReady(true);
|
setScopeReady(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ [useEffect] 初始化文档列表作用域失败:', error);
|
console.error('❌ [useEffect] 初始化文档列表作用域失败:', error);
|
||||||
@@ -421,7 +459,7 @@ export default function DocumentsIndex() {
|
|||||||
setScopeReady(true);
|
setScopeReady(true);
|
||||||
loadingBarService.hide();
|
loadingBarService.hide();
|
||||||
}
|
}
|
||||||
}, [readListScopeFromSession]);
|
}, [persistListScope, readListScopeFromSession]);
|
||||||
|
|
||||||
// 监听 URL 参数变化,重新获取数据
|
// 监听 URL 参数变化,重新获取数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -628,6 +666,11 @@ export default function DocumentsIndex() {
|
|||||||
|
|
||||||
// 下载文档
|
// 下载文档
|
||||||
const handleDownload = (path: string) => {
|
const handleDownload = (path: string) => {
|
||||||
|
if (!path) {
|
||||||
|
toastService.warning('当前版本暂无可下载原文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
|
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
|
||||||
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(path)}`;
|
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(path)}`;
|
||||||
|
|
||||||
@@ -650,7 +693,7 @@ export default function DocumentsIndex() {
|
|||||||
// 删除文档
|
// 删除文档
|
||||||
const handleDelete = (id: string, name: string, fileStatus: string) => {
|
const handleDelete = (id: string, name: string, fileStatus: string) => {
|
||||||
// 禁止删除处理中的文件
|
// 禁止删除处理中的文件
|
||||||
if (fileStatus !== "Processed" && fileStatus !== "Failed") {
|
if (!isDeletableFileStatus(fileStatus)) {
|
||||||
toastService.warning("文件正在处理中,无法删除");
|
toastService.warning("文件正在处理中,无法删除");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -684,7 +727,7 @@ export default function DocumentsIndex() {
|
|||||||
|
|
||||||
// 检查是否有正在处理中的文件
|
// 检查是否有正在处理中的文件
|
||||||
const hasProcessingFiles = documents.some((doc: DocumentUI) =>
|
const hasProcessingFiles = documents.some((doc: DocumentUI) =>
|
||||||
selectedRowKeys.includes(doc.id.toString()) && doc.fileStatus !== 'Processed'
|
selectedRowKeys.includes(doc.id.toString()) && !isDeletableFileStatus(doc.fileStatus)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hasProcessingFiles) {
|
if (hasProcessingFiles) {
|
||||||
@@ -1149,6 +1192,10 @@ export default function DocumentsIndex() {
|
|||||||
|
|
||||||
// 渲染历史版本行的辅助函数
|
// 渲染历史版本行的辅助函数
|
||||||
const renderHistoryRow = (historyDoc: DocumentVersionUI, parentDoc: DocumentUI) => {
|
const renderHistoryRow = (historyDoc: DocumentVersionUI, parentDoc: DocumentUI) => {
|
||||||
|
const canDownloadHistory = Boolean(historyDoc.path);
|
||||||
|
const canShowHistoryStats = hasHistoryResultStats(historyDoc);
|
||||||
|
const canAppendHistoryAssets = parentDoc.type === '1' && historyDoc.fileStatus === 'Processed' && canDownloadHistory;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={`history-${historyDoc.id}`} className="history-row">
|
<tr key={`history-${historyDoc.id}`} className="history-row">
|
||||||
<td className="align-middle px-4 py-3" style={{ width: '50px' }}>
|
<td className="align-middle px-4 py-3" style={{ width: '50px' }}>
|
||||||
@@ -1194,16 +1241,20 @@ export default function DocumentsIndex() {
|
|||||||
})()}
|
})()}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3" style={{ width: '15%' }}>
|
<td className="px-4 py-3" style={{ width: '15%' }}>
|
||||||
<ResultStats
|
{canShowHistoryStats ? (
|
||||||
passCount={historyDoc.pass_count}
|
<ResultStats
|
||||||
warningCount={historyDoc.warning_count}
|
passCount={historyDoc.pass_count}
|
||||||
errorCount={historyDoc.error_count}
|
warningCount={historyDoc.warning_count}
|
||||||
manualCount={historyDoc.manual_count}
|
errorCount={historyDoc.error_count}
|
||||||
previousPassCount={historyDoc.previous_pass_count}
|
manualCount={historyDoc.manual_count}
|
||||||
previousWarningCount={historyDoc.previous_warning_count}
|
previousPassCount={historyDoc.previous_pass_count}
|
||||||
previousErrorCount={historyDoc.previous_error_count}
|
previousWarningCount={historyDoc.previous_warning_count}
|
||||||
previousManualCount={historyDoc.previous_manual_count}
|
previousErrorCount={historyDoc.previous_error_count}
|
||||||
/>
|
previousManualCount={historyDoc.previous_manual_count}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-gray-400">-</span>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-xs text-gray-600 px-4 py-3" style={{ width: '10%' }}>{historyDoc.uploadTime}</td>
|
<td className="text-xs text-gray-600 px-4 py-3" style={{ width: '10%' }}>{historyDoc.uploadTime}</td>
|
||||||
<td className="px-4 py-3" style={{ width: '25%' }}>
|
<td className="px-4 py-3" style={{ width: '25%' }}>
|
||||||
@@ -1230,7 +1281,7 @@ export default function DocumentsIndex() {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{/* 下载按钮 - 需要 document:document:view 权限 */}
|
{/* 下载按钮 - 需要 document:document:view 权限 */}
|
||||||
{canView && (
|
{canView && canDownloadHistory && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-xs px-2 py-1 h-7 mr-1 text-gray-500 hover:underline hover:text-gray-700"
|
className="text-xs px-2 py-1 h-7 mr-1 text-gray-500 hover:underline hover:text-gray-700"
|
||||||
@@ -1241,7 +1292,7 @@ export default function DocumentsIndex() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{/* 追加附件和上传模板按钮 - 需要 document:document:view 权限 */}
|
{/* 追加附件和上传模板按钮 - 需要 document:document:view 权限 */}
|
||||||
{canView && parentDoc.type === '1' && historyDoc.fileStatus === 'Processed' && (
|
{canView && canAppendHistoryAssets && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1272,7 +1323,7 @@ export default function DocumentsIndex() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{/* 删除按钮 - 需要 document:document:view 权限 */}
|
{/* 删除按钮 - 需要 document:document:view 权限 */}
|
||||||
{canView && (
|
{canView && isDeletableFileStatus(historyDoc.fileStatus) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-xs px-2 py-1 h-7 text-error hover:underline hover:text-red-700"
|
className="text-xs px-2 py-1 h-7 text-error hover:underline hover:text-red-700"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
* @author 中国烟草AI合同及卷宗审核系统开发团队
|
* @author 中国烟草AI合同及卷宗审核系统开发团队
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
|
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs, redirect } from "@remix-run/node";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useNavigate, useLoaderData, useFetcher } from "@remix-run/react";
|
import { useNavigate, useLoaderData, useFetcher } from "@remix-run/react";
|
||||||
import reviewsStyles from "~/styles/reviews.css?url";
|
import reviewsStyles from "~/styles/reviews.css?url";
|
||||||
@@ -345,17 +345,24 @@ export const handle = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function loader({ request }: LoaderFunctionArgs): Promise<Response> {
|
export async function loader({ request }: LoaderFunctionArgs): Promise<Response> {
|
||||||
try {
|
const url = new URL(request.url);
|
||||||
const url = new URL(request.url);
|
const id = url.searchParams.get('id') || '';
|
||||||
const id = url.searchParams.get('id') || '';
|
const previousRoute = url.searchParams.get('previousRoute') || '';
|
||||||
const previousRoute = url.searchParams.get('previousRoute') || '';
|
|
||||||
|
|
||||||
|
try {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return Response.json({ result: false, message: '文件ID不能为空', previousRoute });
|
return Response.json({ result: false, message: '文件ID不能为空', previousRoute });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getUserSession } = await import("~/api/login/auth.server");
|
const { getUserSession } = await import("~/api/login/auth.server");
|
||||||
const { userInfo, frontendJWT } = await getUserSession(request);
|
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||||
|
if (!frontendJWT || !userInfo?.role) {
|
||||||
|
throw redirect('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server");
|
||||||
|
await requireRoutePermission('/reviewsTest', userInfo.role, frontendJWT);
|
||||||
|
|
||||||
const reviewData = await getReviewPoints_fromApi(id, request);
|
const reviewData = await getReviewPoints_fromApi(id, request);
|
||||||
|
|
||||||
if ('error' in reviewData && reviewData.error) {
|
if ('error' in reviewData && reviewData.error) {
|
||||||
@@ -382,11 +389,25 @@ export async function loader({ request }: LoaderFunctionArgs): Promise<Response>
|
|||||||
detailMode: 'leaudit',
|
detailMode: 'leaudit',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof Response) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
throw redirect('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.status === 403) {
|
||||||
|
return Response.json({
|
||||||
|
result: false,
|
||||||
|
message: '当前账号没有评查详情访问权限,请联系管理员开通文档查看权限。',
|
||||||
|
previousRoute,
|
||||||
|
}, { status: 403 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.error('[reviewsTest loader] Failed to load review data:', error);
|
console.error('[reviewsTest loader] Failed to load review data:', error);
|
||||||
return Response.json({
|
return Response.json({
|
||||||
result: false,
|
result: false,
|
||||||
message: `获取评查数据失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
message: `获取评查数据失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||||
previousRoute: '',
|
previousRoute,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -621,6 +642,11 @@ export default function ReviewDetails() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDownloadFile = async () => {
|
const handleDownloadFile = async () => {
|
||||||
|
if (!previewPath) {
|
||||||
|
toastService.warning('当前文档暂无可下载原文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(previewPath)}`;
|
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(previewPath)}`;
|
||||||
const response = await axios.get(downloadUrl, { responseType: 'blob' });
|
const response = await axios.get(downloadUrl, { responseType: 'blob' });
|
||||||
@@ -828,8 +854,7 @@ export default function ReviewDetails() {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toastService.success('评查结果已确认,文档审核状态已更新');
|
toastService.success('评查结果已确认,文档审核状态已更新');
|
||||||
// 导航到文档列表页
|
navigate(getReturnUrl());
|
||||||
navigate('/documents/list');
|
|
||||||
} else {
|
} else {
|
||||||
console.error('确认评查结果失败:', result.error);
|
console.error('确认评查结果失败:', result.error);
|
||||||
toastService.error(`确认评查结果失败: ${result.error || '未知错误'}`);
|
toastService.error(`确认评查结果失败: ${result.error || '未知错误'}`);
|
||||||
|
|||||||
+213
-51
@@ -222,6 +222,7 @@ function emptyFieldDraft(group = '基础信息'): FieldDraft {
|
|||||||
name: '',
|
name: '',
|
||||||
type: 'verbatim',
|
type: 'verbatim',
|
||||||
multipleEntities: false,
|
multipleEntities: false,
|
||||||
|
allowed: [],
|
||||||
requiredFrom: 'draft',
|
requiredFrom: 'draft',
|
||||||
description: '',
|
description: '',
|
||||||
};
|
};
|
||||||
@@ -245,9 +246,13 @@ function emptyVisualDraft(type = '签章'): VisualDraft {
|
|||||||
name: '',
|
name: '',
|
||||||
type,
|
type,
|
||||||
required: 'true',
|
required: 'true',
|
||||||
|
requiredFrom: '',
|
||||||
signerRoles: [],
|
signerRoles: [],
|
||||||
signatureTypes: [],
|
signatureTypes: [],
|
||||||
privateSealRestricted: false,
|
privateSealRestricted: false,
|
||||||
|
expectedMatchField: '',
|
||||||
|
expectedMatchAlternatives: [],
|
||||||
|
prompt: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,6 +512,7 @@ export default function RulesTestDetail() {
|
|||||||
const [fieldDraft, setFieldDraft] = useState<FieldDraft>(emptyFieldDraft(pack.fields[0]?.group || '基础信息'));
|
const [fieldDraft, setFieldDraft] = useState<FieldDraft>(emptyFieldDraft(pack.fields[0]?.group || '基础信息'));
|
||||||
const [documentDraft, setDocumentDraft] = useState<DocumentDraft>(emptyDocumentDraft());
|
const [documentDraft, setDocumentDraft] = useState<DocumentDraft>(emptyDocumentDraft());
|
||||||
const [visualDraft, setVisualDraft] = useState<VisualDraft>(emptyVisualDraft(pack.visualElements[0]?.type || '签章'));
|
const [visualDraft, setVisualDraft] = useState<VisualDraft>(emptyVisualDraft(pack.visualElements[0]?.type || '签章'));
|
||||||
|
const [versionItems, setVersionItems] = useState<RuleVersionItem[]>(versions);
|
||||||
const [selectedRollbackVersionId, setSelectedRollbackVersionId] = useState('');
|
const [selectedRollbackVersionId, setSelectedRollbackVersionId] = useState('');
|
||||||
const [dependencyDialogOpen, setDependencyDialogOpen] = useState(false);
|
const [dependencyDialogOpen, setDependencyDialogOpen] = useState(false);
|
||||||
const [dependencySearch, setDependencySearch] = useState('');
|
const [dependencySearch, setDependencySearch] = useState('');
|
||||||
@@ -524,6 +530,7 @@ export default function RulesTestDetail() {
|
|||||||
setFields(pack.fields);
|
setFields(pack.fields);
|
||||||
setSubDocuments(pack.subDocuments);
|
setSubDocuments(pack.subDocuments);
|
||||||
setVisualElements(pack.visualElements);
|
setVisualElements(pack.visualElements);
|
||||||
|
setVersionItems(versions);
|
||||||
setSelectedRuleKey(requestedRuleId || ruleKey(pack.rules[0] || { id: '', ruleId: '' }));
|
setSelectedRuleKey(requestedRuleId || ruleKey(pack.rules[0] || { id: '', ruleId: '' }));
|
||||||
setEditor(null);
|
setEditor(null);
|
||||||
setDependencyDialogOpen(false);
|
setDependencyDialogOpen(false);
|
||||||
@@ -535,7 +542,7 @@ export default function RulesTestDetail() {
|
|||||||
setDraftSaved(false);
|
setDraftSaved(false);
|
||||||
setSaveMessage('');
|
setSaveMessage('');
|
||||||
setSaveError('');
|
setSaveError('');
|
||||||
}, [pack.id, requestedRuleId]);
|
}, [pack.currentVersionId, pack.fallbackVersionId, pack.id, pack.resolvedVersionId, pack.yamlSource, requestedRuleId, versions]);
|
||||||
|
|
||||||
const currentRule = useMemo(() => {
|
const currentRule = useMemo(() => {
|
||||||
return rules.find(rule => rule.id === selectedRuleKey || rule.ruleId === selectedRuleKey) || rules[0];
|
return rules.find(rule => rule.id === selectedRuleKey || rule.ruleId === selectedRuleKey) || rules[0];
|
||||||
@@ -640,46 +647,47 @@ export default function RulesTestDetail() {
|
|||||||
const rulesById = useMemo(() => new Map(rules.map(rule => [rule.ruleId || rule.id, rule])), [rules]);
|
const rulesById = useMemo(() => new Map(rules.map(rule => [rule.ruleId || rule.id, rule])), [rules]);
|
||||||
const saveButtonBusy = saveFetcher.state !== 'idle';
|
const saveButtonBusy = saveFetcher.state !== 'idle';
|
||||||
const latestDraftVersion = useMemo(
|
const latestDraftVersion = useMemo(
|
||||||
() => versions.find((item) => !['published', 'rollback'].includes(item.status)),
|
() => versionItems.find((item) => !['published', 'rollback'].includes(item.status)),
|
||||||
[versions],
|
[versionItems],
|
||||||
);
|
);
|
||||||
const rollbackVersionOptions = useMemo(
|
const rollbackVersionOptions = useMemo(
|
||||||
() => versions,
|
() => versionItems,
|
||||||
[versions],
|
[versionItems],
|
||||||
);
|
);
|
||||||
const rollbackOptions = useMemo(
|
const rollbackOptions = useMemo(
|
||||||
() => rollbackVersionOptions.filter((item) => item.id !== pack.currentVersionId),
|
() => rollbackVersionOptions.filter((item) => item.id !== pack.currentVersionId),
|
||||||
[rollbackVersionOptions, pack.currentVersionId],
|
[rollbackVersionOptions, pack.currentVersionId],
|
||||||
);
|
);
|
||||||
const rollbackTargetVersion = useMemo(
|
|
||||||
() => rollbackOptions.find((item) => String(item.id) === selectedRollbackVersionId) || rollbackOptions[0] || null,
|
|
||||||
[rollbackOptions, selectedRollbackVersionId],
|
|
||||||
);
|
|
||||||
const packFilterMainType = pack.businessType || pack.mainType;
|
const packFilterMainType = pack.businessType || pack.mainType;
|
||||||
const currentResolvedVersion = useMemo(
|
const currentResolvedVersion = useMemo(
|
||||||
() => {
|
() => {
|
||||||
if (pack.currentVersionId) {
|
if (pack.currentVersionId) {
|
||||||
return versions.find((item) => item.id === pack.currentVersionId) || null;
|
return versionItems.find((item) => item.id === pack.currentVersionId) || null;
|
||||||
}
|
}
|
||||||
if (pack.fallbackVersionId) {
|
if (pack.fallbackVersionId) {
|
||||||
return versions.find((item) => item.id === pack.fallbackVersionId) || null;
|
return versionItems.find((item) => item.id === pack.fallbackVersionId) || null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[pack.currentVersionId, pack.fallbackVersionId, versions],
|
[pack.currentVersionId, pack.fallbackVersionId, versionItems],
|
||||||
);
|
);
|
||||||
const versionStatusLabel = (status: string | undefined) => {
|
const versionStatusLabel = (status: string | undefined) => {
|
||||||
const normalized = String(status || '').trim().toLowerCase();
|
const normalized = String(status || '').trim().toLowerCase();
|
||||||
if (normalized === 'published') return '已发布';
|
if (normalized === 'published') return '已发布';
|
||||||
if (normalized === 'rollback') return '回滚版本';
|
if (normalized === 'rollback') return '被回滚替换';
|
||||||
if (normalized === 'draft') return '草稿';
|
if (normalized === 'draft') return '草稿';
|
||||||
if (normalized === 'deprecated') return '已废弃';
|
if (normalized === 'deprecated') return '历史废弃版本';
|
||||||
return status || '-';
|
return status || '-';
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedRollbackVersionId('');
|
const hasSelectedVersion = rollbackOptions.some((item) => String(item.id) === selectedRollbackVersionId);
|
||||||
}, [pack.id, pack.currentVersionId, versions.length]);
|
const selectedIsCurrent = selectedRollbackVersionId !== '' && String(pack.currentVersionId || '') === selectedRollbackVersionId;
|
||||||
|
if (hasSelectedVersion && !selectedIsCurrent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedRollbackVersionId(rollbackOptions[0] ? String(rollbackOptions[0].id) : '');
|
||||||
|
}, [pack.currentVersionId, rollbackOptions, selectedRollbackVersionId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!saveFetcher.data) return;
|
if (!saveFetcher.data) return;
|
||||||
@@ -687,12 +695,35 @@ export default function RulesTestDetail() {
|
|||||||
setDraftSaved(saveFetcher.data.intent === 'save');
|
setDraftSaved(saveFetcher.data.intent === 'save');
|
||||||
setSaveError('');
|
setSaveError('');
|
||||||
if (saveFetcher.data.intent === 'save') {
|
if (saveFetcher.data.intent === 'save') {
|
||||||
|
if (saveFetcher.data.versionId) {
|
||||||
|
setVersionItems((current) => {
|
||||||
|
const nextVersion: RuleVersionItem = {
|
||||||
|
id: saveFetcher.data?.versionId || 0,
|
||||||
|
ruleSetId: current[0]?.ruleSetId || 0,
|
||||||
|
versionNo: saveFetcher.data?.versionNo || '-',
|
||||||
|
status: 'draft',
|
||||||
|
ossUrl: current.find((item) => item.id === saveFetcher.data?.versionId)?.ossUrl || '',
|
||||||
|
changeNote: 'rulesTest.detail 保存评查点草稿',
|
||||||
|
publishedAt: current.find((item) => item.id === saveFetcher.data?.versionId)?.publishedAt || null,
|
||||||
|
};
|
||||||
|
const existed = current.some((item) => item.id === nextVersion.id);
|
||||||
|
if (existed) {
|
||||||
|
return current.map((item) => (item.id === nextVersion.id ? { ...item, ...nextVersion } : item));
|
||||||
|
}
|
||||||
|
return [nextVersion, ...current];
|
||||||
|
});
|
||||||
|
}
|
||||||
setSaveMessage(saveFetcher.data.versionNo
|
setSaveMessage(saveFetcher.data.versionNo
|
||||||
? `规则草稿已保存为版本 ${saveFetcher.data.versionNo}`
|
? `规则草稿已保存为版本 ${saveFetcher.data.versionNo}`
|
||||||
: saveFetcher.data.message || '规则草稿已保存');
|
: saveFetcher.data.message || '规则草稿已保存');
|
||||||
} else {
|
return;
|
||||||
setSaveMessage(saveFetcher.data.message || (saveFetcher.data.intent === 'publish' ? '规则版本已发布' : '规则版本已回滚'));
|
|
||||||
}
|
}
|
||||||
|
if (saveFetcher.data.intent === 'publish') {
|
||||||
|
setSaveMessage(saveFetcher.data.message || (saveFetcher.data.intent === 'publish' ? '规则版本已发布' : '规则版本已回滚'));
|
||||||
|
revalidator.revalidate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSaveMessage(saveFetcher.data.message || '规则版本已回滚');
|
||||||
revalidator.revalidate();
|
revalidator.revalidate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -839,6 +870,7 @@ export default function RulesTestDetail() {
|
|||||||
group: fieldDraft.group || '未分组',
|
group: fieldDraft.group || '未分组',
|
||||||
requiredFrom: fieldDraft.requiredFrom || 'draft',
|
requiredFrom: fieldDraft.requiredFrom || 'draft',
|
||||||
type: fieldDraft.type || 'verbatim',
|
type: fieldDraft.type || 'verbatim',
|
||||||
|
allowed: fieldDraft.type === 'enum' ? (fieldDraft.allowed || []).map((item) => String(item || '').trim()).filter(Boolean) : [],
|
||||||
description: fieldDraft.description || '',
|
description: fieldDraft.description || '',
|
||||||
};
|
};
|
||||||
setFields((current) => editor.mode === 'edit'
|
setFields((current) => editor.mode === 'edit'
|
||||||
@@ -879,6 +911,7 @@ export default function RulesTestDetail() {
|
|||||||
group: field.group || '未分组',
|
group: field.group || '未分组',
|
||||||
requiredFrom: field.requiredFrom || '-',
|
requiredFrom: field.requiredFrom || '-',
|
||||||
type: field.type || 'verbatim',
|
type: field.type || 'verbatim',
|
||||||
|
allowed: field.type === 'enum' ? (field.allowed || []).map((item) => String(item || '').trim()).filter(Boolean) : [],
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
setSubDocuments((current) => editor.mode === 'edit'
|
setSubDocuments((current) => editor.mode === 'edit'
|
||||||
@@ -927,6 +960,7 @@ export default function RulesTestDetail() {
|
|||||||
name: '',
|
name: '',
|
||||||
type: 'verbatim',
|
type: 'verbatim',
|
||||||
multipleEntities: false,
|
multipleEntities: false,
|
||||||
|
allowed: [],
|
||||||
requiredFrom: '-',
|
requiredFrom: '-',
|
||||||
description: '',
|
description: '',
|
||||||
},
|
},
|
||||||
@@ -951,15 +985,20 @@ export default function RulesTestDetail() {
|
|||||||
const previousVisual = editor.mode === 'edit'
|
const previousVisual = editor.mode === 'edit'
|
||||||
? visualElements.find((item) => item.id === editor.id)
|
? visualElements.find((item) => item.id === editor.id)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const normalizedType = visualDraft.type || '签章';
|
||||||
const normalizedVisual: VisualElementSummary = {
|
const normalizedVisual: VisualElementSummary = {
|
||||||
...visualDraft,
|
...visualDraft,
|
||||||
id: visualDraft.id || makeId('visual'),
|
id: visualDraft.id || makeId('visual'),
|
||||||
name: visualDraft.name || visualDraft.id,
|
name: visualDraft.name || visualDraft.id,
|
||||||
type: visualDraft.type || '签章',
|
type: normalizedType,
|
||||||
required: visualDraft.required || 'true',
|
required: visualDraft.required || 'true',
|
||||||
signerRoles: (visualDraft.signerRoles || []).filter(Boolean),
|
requiredFrom: visualDraft.requiredFrom || '',
|
||||||
|
signerRoles: normalizedType === '签名' ? (visualDraft.signerRoles || []).filter(Boolean) : [],
|
||||||
signatureTypes: (visualDraft.signatureTypes || []).filter(Boolean),
|
signatureTypes: (visualDraft.signatureTypes || []).filter(Boolean),
|
||||||
privateSealRestricted: Boolean(visualDraft.privateSealRestricted),
|
privateSealRestricted: normalizedType === '签名' ? Boolean(visualDraft.privateSealRestricted) : false,
|
||||||
|
expectedMatchField: visualDraft.expectedMatchField || '',
|
||||||
|
expectedMatchAlternatives: (visualDraft.expectedMatchAlternatives || []).filter(Boolean),
|
||||||
|
prompt: normalizedType === '骑缝章' ? (visualDraft.prompt || '') : '',
|
||||||
};
|
};
|
||||||
setVisualElements((current) => editor.mode === 'edit'
|
setVisualElements((current) => editor.mode === 'edit'
|
||||||
? current.map((item) => (item.id === editor.id ? normalizedVisual : item))
|
? current.map((item) => (item.id === editor.id ? normalizedVisual : item))
|
||||||
@@ -974,7 +1013,7 @@ export default function RulesTestDetail() {
|
|||||||
previousVisual ? { from: `visual.${previousVisual.id}`, to: `visual.${normalizedVisual.id}` } : null,
|
previousVisual ? { from: `visual.${previousVisual.id}`, to: `visual.${normalizedVisual.id}` } : null,
|
||||||
previousVisual ? { from: `visual.${previousVisual.name || previousVisual.id}`, to: `visual.${normalizedVisual.name || normalizedVisual.id}` } : null,
|
previousVisual ? { from: `visual.${previousVisual.name || previousVisual.id}`, to: `visual.${normalizedVisual.name || normalizedVisual.id}` } : null,
|
||||||
].filter(Boolean) as Array<{ from: string; to: string }>,
|
].filter(Boolean) as Array<{ from: string; to: string }>,
|
||||||
[`visual.${normalizedVisual.id}`],
|
[`visual.${normalizedVisual.name || normalizedVisual.id}`],
|
||||||
);
|
);
|
||||||
setEditor(null);
|
setEditor(null);
|
||||||
};
|
};
|
||||||
@@ -1027,6 +1066,7 @@ export default function RulesTestDetail() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const rollbackRuleVersion = () => {
|
const rollbackRuleVersion = () => {
|
||||||
|
const rollbackTargetVersion = rollbackOptions.find((item) => String(item.id) === selectedRollbackVersionId) || null;
|
||||||
if (!rollbackTargetVersion) {
|
if (!rollbackTargetVersion) {
|
||||||
setSaveError('当前没有可回滚的历史可用版本。');
|
setSaveError('当前没有可回滚的历史可用版本。');
|
||||||
setSaveMessage('');
|
setSaveMessage('');
|
||||||
@@ -1119,7 +1159,7 @@ export default function RulesTestDetail() {
|
|||||||
</button>
|
</button>
|
||||||
<select
|
<select
|
||||||
className="rules-version-select"
|
className="rules-version-select"
|
||||||
value={rollbackTargetVersion ? String(rollbackTargetVersion.id) : selectedRollbackVersionId}
|
value={selectedRollbackVersionId}
|
||||||
onChange={(event) => setSelectedRollbackVersionId(event.target.value)}
|
onChange={(event) => setSelectedRollbackVersionId(event.target.value)}
|
||||||
disabled={saveButtonBusy || rollbackVersionOptions.length === 0}
|
disabled={saveButtonBusy || rollbackVersionOptions.length === 0}
|
||||||
>
|
>
|
||||||
@@ -1131,11 +1171,11 @@ export default function RulesTestDetail() {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<button type="button" className="ant-btn ant-btn-default" disabled={saveButtonBusy || !rollbackTargetVersion} onClick={rollbackRuleVersion}>
|
<button type="button" className="ant-btn ant-btn-default" disabled={saveButtonBusy || !selectedRollbackVersionId} onClick={rollbackRuleVersion}>
|
||||||
<i className="ri-history-line mr-1.5"></i>回滚版本
|
<i className="ri-history-line mr-1.5"></i>回滚版本
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="ant-btn ant-btn-primary" disabled={!currentRule} onClick={() => currentRule && openRuleEditor(currentRule)}>
|
<button type="button" className="ant-btn ant-btn-primary" onClick={() => currentRule ? openRuleEditor(currentRule) : openRuleEditor()}>
|
||||||
<i className="ri-edit-line mr-1.5"></i>编辑评查点
|
<i className={`${currentRule ? 'ri-edit-line' : 'ri-add-line'} mr-1.5`}></i>{currentRule ? '编辑评查点' : '新增评查点'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1305,7 +1345,7 @@ export default function RulesTestDetail() {
|
|||||||
<div key={item.id} className="config-item-card">
|
<div key={item.id} className="config-item-card">
|
||||||
<div className="config-item-main">
|
<div className="config-item-main">
|
||||||
<strong>{item.name || item.id}</strong>
|
<strong>{item.name || item.id}</strong>
|
||||||
<span>{item.type} / {item.id}</span>
|
<span>{item.type}</span>
|
||||||
<span>{requiredFromLabel(String(item.required))}{item.signatureTypes && item.signatureTypes.length > 0 ? ` · ${item.signatureTypes.join('、')}` : ''}</span>
|
<span>{requiredFromLabel(String(item.required))}{item.signatureTypes && item.signatureTypes.length > 0 ? ` · ${item.signatureTypes.join('、')}` : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="config-item-actions">
|
<div className="config-item-actions">
|
||||||
@@ -1421,9 +1461,101 @@ export default function RulesTestDetail() {
|
|||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Card className="ant-card">
|
<>
|
||||||
<div className="empty-state">当前链接没有匹配到评查点,请返回列表重新进入。</div>
|
<Card className="ant-card">
|
||||||
</Card>
|
<div className="empty-state">
|
||||||
|
当前子类型还没有正式评查点。你可以先新增评查点,或先维护抽取字段 / 子文档 / 视觉要素后再回到这里补规则。
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="ant-card" title="抽取字段">
|
||||||
|
<div className="config-section-tools">
|
||||||
|
<span className="config-section-tip">这里维护当前子类型可复用的抽取字段;新增评查点后再在“依赖字段”中引用。</span>
|
||||||
|
<button type="button" className="ant-btn ant-btn-default" onClick={() => openFieldEditor()}>
|
||||||
|
<i className="ri-add-line mr-1.5"></i>新增字段
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{fields.length > 0 ? (
|
||||||
|
<div className="config-item-list">
|
||||||
|
{fields.map((field) => (
|
||||||
|
<div key={field.id} className="config-item-card">
|
||||||
|
<div className="config-item-main">
|
||||||
|
<strong>{field.name}</strong>
|
||||||
|
<span>{field.group || '未分组'} / {fieldTypeLabel(field.type)}</span>
|
||||||
|
<span>
|
||||||
|
{requiredFromLabel(field.requiredFrom || '-')}
|
||||||
|
{field.type === 'enum' && field.allowed && field.allowed.length > 0 ? ` · ${field.allowed.join('、')}` : ''}
|
||||||
|
{field.description ? ` · ${field.description}` : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="config-item-actions">
|
||||||
|
<button type="button" className="ant-btn ant-btn-default" onClick={() => openFieldEditor(field)}>编辑</button>
|
||||||
|
<button type="button" className="ant-btn ant-btn-default" onClick={() => removeField(field.id)}>删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="empty-state">当前还没有配置抽取字段。</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="ant-card" title="子文档 / 文书">
|
||||||
|
<div className="config-section-tools">
|
||||||
|
<span className="config-section-tip">案卷场景可先把常用文书与内部字段配好,后续评查点直接引用。</span>
|
||||||
|
<button type="button" className="ant-btn ant-btn-default" onClick={() => openDocumentEditor()}>
|
||||||
|
<i className="ri-add-line mr-1.5"></i>新增子文档
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{subDocuments.length > 0 ? (
|
||||||
|
<div className="config-item-list">
|
||||||
|
{subDocuments.map((document) => (
|
||||||
|
<div key={document.id} className="config-item-card">
|
||||||
|
<div className="config-item-main">
|
||||||
|
<strong>{document.name}</strong>
|
||||||
|
<span>{document.id} / {requiredFromLabel(document.required || '-')}</span>
|
||||||
|
<span>{document.fields.length} 个字段{document.groups.length > 0 ? ` · ${document.groups.join('、')}` : ''}</span>
|
||||||
|
</div>
|
||||||
|
<div className="config-item-actions">
|
||||||
|
<button type="button" className="ant-btn ant-btn-default" onClick={() => openDocumentEditor(document)}>编辑</button>
|
||||||
|
<button type="button" className="ant-btn ant-btn-default" onClick={() => removeDocument(document.id)}>删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="empty-state">当前还没有配置子文档。</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="ant-card" title="视觉要素">
|
||||||
|
<div className="config-section-tools">
|
||||||
|
<span className="config-section-tip">这里维护签章、签名、骑缝章等可复用视觉要素,新增评查点后再按需引用。</span>
|
||||||
|
<button type="button" className="ant-btn ant-btn-default" onClick={() => openVisualEditor()}>
|
||||||
|
<i className="ri-add-line mr-1.5"></i>新增视觉要素
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{visualElements.length > 0 ? (
|
||||||
|
<div className="config-item-list">
|
||||||
|
{visualElements.map((item) => (
|
||||||
|
<div key={item.id} className="config-item-card">
|
||||||
|
<div className="config-item-main">
|
||||||
|
<strong>{item.name || item.id}</strong>
|
||||||
|
<span>{item.type}</span>
|
||||||
|
<span>{requiredFromLabel(String(item.required))}{item.signatureTypes && item.signatureTypes.length > 0 ? ` · ${item.signatureTypes.join('、')}` : ''}</span>
|
||||||
|
</div>
|
||||||
|
<div className="config-item-actions">
|
||||||
|
<button type="button" className="ant-btn ant-btn-default" onClick={() => openVisualEditor(item)}>编辑</button>
|
||||||
|
<button type="button" className="ant-btn ant-btn-default" onClick={() => removeVisual(item.id)}>删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="empty-state">当前还没有配置视觉要素。</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1603,7 +1735,7 @@ export default function RulesTestDetail() {
|
|||||||
<div className="drawer-grid">
|
<div className="drawer-grid">
|
||||||
<label>
|
<label>
|
||||||
<span>字段类型</span>
|
<span>字段类型</span>
|
||||||
<select value={fieldDraft.type} onChange={(event) => setFieldDraft({ ...fieldDraft, type: event.target.value })}>
|
<select value={fieldDraft.type} onChange={(event) => setFieldDraft({ ...fieldDraft, type: event.target.value, allowed: event.target.value === 'enum' ? (fieldDraft.allowed || []) : [] })}>
|
||||||
{['verbatim', 'string', 'money', 'date', 'enum', 'number'].map((type) => <option key={type} value={type}>{fieldTypeLabel(type)}</option>)}
|
{['verbatim', 'string', 'money', 'date', 'enum', 'number'].map((type) => <option key={type} value={type}>{fieldTypeLabel(type)}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -1614,6 +1746,16 @@ export default function RulesTestDetail() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
{fieldDraft.type === 'enum' && (
|
||||||
|
<label>
|
||||||
|
<span>可选值</span>
|
||||||
|
<input
|
||||||
|
value={(fieldDraft.allowed || []).join(',')}
|
||||||
|
onChange={(event) => setFieldDraft({ ...fieldDraft, allowed: event.target.value.split(/[,,]/).map((item) => item.trim()).filter(Boolean) })}
|
||||||
|
placeholder="如:有,无;男,女"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
<label>
|
<label>
|
||||||
<span>字段说明</span>
|
<span>字段说明</span>
|
||||||
<textarea value={fieldDraft.description} onChange={(event) => setFieldDraft({ ...fieldDraft, description: event.target.value })} placeholder="描述字段如何抽取、给规则如何引用" />
|
<textarea value={fieldDraft.description} onChange={(event) => setFieldDraft({ ...fieldDraft, description: event.target.value })} placeholder="描述字段如何抽取、给规则如何引用" />
|
||||||
@@ -1658,12 +1800,19 @@ export default function RulesTestDetail() {
|
|||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
value={field.type || 'verbatim'}
|
value={field.type || 'verbatim'}
|
||||||
onChange={(event) => updateDocumentField(field.id, { type: event.target.value })}
|
onChange={(event) => updateDocumentField(field.id, { type: event.target.value, allowed: event.target.value === 'enum' ? (field.allowed || []) : [] })}
|
||||||
>
|
>
|
||||||
{['verbatim', 'string', 'money', 'date', 'enum', 'number'].map((type) => (
|
{['verbatim', 'string', 'money', 'date', 'enum', 'number'].map((type) => (
|
||||||
<option key={type} value={type}>{fieldTypeLabel(type)}</option>
|
<option key={type} value={type}>{fieldTypeLabel(type)}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
{field.type === 'enum' && (
|
||||||
|
<input
|
||||||
|
value={(field.allowed || []).join(',')}
|
||||||
|
onChange={(event) => updateDocumentField(field.id, { allowed: event.target.value.split(/[,,]/).map((item) => item.trim()).filter(Boolean) })}
|
||||||
|
placeholder="可选值"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
value={field.description || ''}
|
value={field.description || ''}
|
||||||
onChange={(event) => updateDocumentField(field.id, { description: event.target.value })}
|
onChange={(event) => updateDocumentField(field.id, { description: event.target.value })}
|
||||||
@@ -1697,7 +1846,16 @@ export default function RulesTestDetail() {
|
|||||||
<div className="drawer-grid">
|
<div className="drawer-grid">
|
||||||
<label>
|
<label>
|
||||||
<span>要素类型</span>
|
<span>要素类型</span>
|
||||||
<select value={visualDraft.type} onChange={(event) => setVisualDraft({ ...visualDraft, type: event.target.value })}>
|
<select
|
||||||
|
value={visualDraft.type}
|
||||||
|
onChange={(event) => setVisualDraft((current) => ({
|
||||||
|
...current,
|
||||||
|
type: event.target.value,
|
||||||
|
signerRoles: event.target.value === '签名' ? current.signerRoles || [] : [],
|
||||||
|
privateSealRestricted: event.target.value === '签名' ? Boolean(current.privateSealRestricted) : false,
|
||||||
|
prompt: event.target.value === '骑缝章' ? current.prompt || '' : '',
|
||||||
|
}))}
|
||||||
|
>
|
||||||
{['签章', '签名', '骑缝章'].map((type) => <option key={type} value={type}>{type}</option>)}
|
{['签章', '签名', '骑缝章'].map((type) => <option key={type} value={type}>{type}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -1709,29 +1867,33 @@ export default function RulesTestDetail() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label>
|
||||||
<span>签章类型(逗号分隔)</span>
|
<span>{visualDraft.type === '签名' ? '签名类型(逗号分隔)' : '签章类型(逗号分隔)'}</span>
|
||||||
<input
|
<input
|
||||||
value={(visualDraft.signatureTypes || []).join(',')}
|
value={(visualDraft.signatureTypes || []).join(',')}
|
||||||
onChange={(event) => setVisualDraft({ ...visualDraft, signatureTypes: event.target.value.split(/[,,]/).map((item) => item.trim()).filter(Boolean) })}
|
onChange={(event) => setVisualDraft({ ...visualDraft, signatureTypes: event.target.value.split(/[,,]/).map((item) => item.trim()).filter(Boolean) })}
|
||||||
placeholder="如:合同专用章,公章"
|
placeholder={visualDraft.type === '签名' ? '如:签名,私章' : '如:合同专用章,公章'}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
{visualDraft.type === '签名' && (
|
||||||
<span>签署角色(逗号分隔)</span>
|
<>
|
||||||
<input
|
<label>
|
||||||
value={(visualDraft.signerRoles || []).join(',')}
|
<span>签署角色(逗号分隔)</span>
|
||||||
onChange={(event) => setVisualDraft({ ...visualDraft, signerRoles: event.target.value.split(/[,,]/).map((item) => item.trim()).filter(Boolean) })}
|
<input
|
||||||
placeholder="如:甲方,乙方,承办人"
|
value={(visualDraft.signerRoles || []).join(',')}
|
||||||
/>
|
onChange={(event) => setVisualDraft({ ...visualDraft, signerRoles: event.target.value.split(/[,,]/).map((item) => item.trim()).filter(Boolean) })}
|
||||||
</label>
|
placeholder="如:甲方,乙方,承办人"
|
||||||
<label className="drawer-checkbox-row">
|
/>
|
||||||
<input
|
</label>
|
||||||
type="checkbox"
|
<label className="drawer-checkbox-row">
|
||||||
checked={Boolean(visualDraft.privateSealRestricted)}
|
<input
|
||||||
onChange={(event) => setVisualDraft({ ...visualDraft, privateSealRestricted: event.target.checked })}
|
type="checkbox"
|
||||||
/>
|
checked={Boolean(visualDraft.privateSealRestricted)}
|
||||||
<span>限制为私章 / 私人签章场景</span>
|
onChange={(event) => setVisualDraft({ ...visualDraft, privateSealRestricted: event.target.checked })}
|
||||||
</label>
|
/>
|
||||||
|
<span>限制为私章 / 私人签章场景</span>
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className="drawer-actions">
|
<div className="drawer-actions">
|
||||||
<Button type="default" onClick={() => setEditor(null)}>取消</Button>
|
<Button type="default" onClick={() => setEditor(null)}>取消</Button>
|
||||||
<Button type="primary" onClick={saveVisual}>保存视觉要素</Button>
|
<Button type="primary" onClick={saveVisual}>保存视觉要素</Button>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { FilterPanel, FilterSelect, SearchFilter } from '~/components/ui/FilterP
|
|||||||
import { Pagination } from '~/components/ui/Pagination';
|
import { Pagination } from '~/components/ui/Pagination';
|
||||||
import { Table } from '~/components/ui/Table';
|
import { Table } from '~/components/ui/Table';
|
||||||
import { Tag, type TagColor } from '~/components/ui/Tag';
|
import { Tag, type TagColor } from '~/components/ui/Tag';
|
||||||
import { loadRuleConfigPacks } from '~/utils/rules-config-packs.server';
|
import { loadRuleConfigPackSummaries, type RuleConfigPackSummary } from '~/utils/rules-config-packs.server';
|
||||||
import type { RuleSummary, RuleYamlPack } from '~/utils/rules-yaml-mock.server';
|
import type { RuleSummary } from '~/utils/rules-yaml-mock.server';
|
||||||
import styles from '~/styles/pages/rules_test.css?url';
|
import styles from '~/styles/pages/rules_test.css?url';
|
||||||
|
|
||||||
export const links = () => [
|
export const links = () => [
|
||||||
@@ -31,12 +31,12 @@ type RuleRow = RuleSummary & {
|
|||||||
mainType: string;
|
mainType: string;
|
||||||
subtype: string;
|
subtype: string;
|
||||||
yamlName: string;
|
yamlName: string;
|
||||||
yamlStatus: RuleYamlPack['sourceStatus'];
|
yamlStatus: RuleConfigPackSummary['sourceStatus'];
|
||||||
|
isPlaceholder?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LoaderData = {
|
type LoaderData = {
|
||||||
rows: RuleRow[];
|
rows: RuleRow[];
|
||||||
packs: RuleYamlPack[];
|
|
||||||
filters: {
|
filters: {
|
||||||
documentType: string;
|
documentType: string;
|
||||||
mainType: string;
|
mainType: string;
|
||||||
@@ -61,7 +61,7 @@ function unique(values: string[]): string[] {
|
|||||||
return Array.from(new Set(values.filter(Boolean)));
|
return Array.from(new Set(values.filter(Boolean)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDocumentScope(pack: Pick<RuleYamlPack, 'documentType' | 'mainType' | 'moduleType'>): string {
|
function resolveDocumentScope(pack: Pick<RuleConfigPackSummary, 'documentType' | 'mainType' | 'moduleType'>): string {
|
||||||
const values = [pack.documentType, pack.mainType, pack.moduleType].join(' ');
|
const values = [pack.documentType, pack.mainType, pack.moduleType].join(' ');
|
||||||
if (values.includes('合同')) return '合同';
|
if (values.includes('合同')) return '合同';
|
||||||
if (values.includes('案卷') || values.includes('卷宗') || values.includes('行政处罚') || values.includes('行政许可')) {
|
if (values.includes('案卷') || values.includes('卷宗') || values.includes('行政处罚') || values.includes('行政许可')) {
|
||||||
@@ -71,7 +71,7 @@ function resolveDocumentScope(pack: Pick<RuleYamlPack, 'documentType' | 'mainTyp
|
|||||||
return pack.documentType || pack.mainType || pack.moduleType || '未分类';
|
return pack.documentType || pack.mainType || pack.moduleType || '未分类';
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveBusinessType(pack: Pick<RuleYamlPack, 'businessType' | 'mainType'>): string {
|
function resolveBusinessType(pack: Pick<RuleConfigPackSummary, 'businessType' | 'mainType'>): string {
|
||||||
return pack.businessType || pack.mainType || '';
|
return pack.businessType || pack.mainType || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
pageSize: [10, 20, 30, 50].includes(requestedPageSize) ? requestedPageSize : 10
|
pageSize: [10, 20, 30, 50].includes(requestedPageSize) ? requestedPageSize : 10
|
||||||
};
|
};
|
||||||
|
|
||||||
const packs = await loadRuleConfigPacks(request);
|
const packs = await loadRuleConfigPackSummaries(request);
|
||||||
const packScopes = packs.map(pack => ({
|
const packScopes = packs.map(pack => ({
|
||||||
pack,
|
pack,
|
||||||
scope: resolveDocumentScope(pack),
|
scope: resolveDocumentScope(pack),
|
||||||
@@ -158,11 +158,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
moduleType: pack.moduleType,
|
moduleType: pack.moduleType,
|
||||||
mainType: resolveBusinessType(pack),
|
mainType: resolveBusinessType(pack),
|
||||||
subtype: pack.subtype,
|
subtype: pack.subtype,
|
||||||
yamlName: pack.metadata.name || '待配置 YAML',
|
yamlName: pack.yamlName || '待配置 YAML',
|
||||||
yamlStatus: pack.sourceStatus,
|
yamlStatus: pack.sourceStatus,
|
||||||
id: `${pack.id}-empty`,
|
id: `${pack.id}-empty`,
|
||||||
ruleId: '-',
|
ruleId: '-',
|
||||||
name: '暂无规则配置',
|
name: `${pack.subtype}待配置`,
|
||||||
group: '待配置',
|
group: '待配置',
|
||||||
risk: '-',
|
risk: '-',
|
||||||
score: '-',
|
score: '-',
|
||||||
@@ -176,7 +176,10 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
stageCount: 0,
|
stageCount: 0,
|
||||||
appliesIn: [],
|
appliesIn: [],
|
||||||
prompt: '',
|
prompt: '',
|
||||||
description: '当前文档类型已保留规则列表与 YAML 配置页流程,等待后续接入规则文件。'
|
description: pack.sourceStatus === 'missing'
|
||||||
|
? '当前规则集已建立,但生效版本正文暂未成功加载,请进入配置页检查并重新保存。'
|
||||||
|
: '当前子类型还没有正式评查点,请进入配置页补充字段、子文档与评查规则。',
|
||||||
|
isPlaceholder: true,
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +191,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
moduleType: pack.moduleType,
|
moduleType: pack.moduleType,
|
||||||
mainType: resolveBusinessType(pack),
|
mainType: resolveBusinessType(pack),
|
||||||
subtype: pack.subtype,
|
subtype: pack.subtype,
|
||||||
yamlName: pack.metadata.name,
|
yamlName: pack.yamlName,
|
||||||
yamlStatus: pack.sourceStatus
|
yamlStatus: pack.sourceStatus
|
||||||
}));
|
}));
|
||||||
}).filter(row => {
|
}).filter(row => {
|
||||||
@@ -207,7 +210,6 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
|
|
||||||
return Response.json({
|
return Response.json({
|
||||||
rows,
|
rows,
|
||||||
packs,
|
|
||||||
filters: {
|
filters: {
|
||||||
...filters,
|
...filters,
|
||||||
page: currentPage
|
page: currentPage
|
||||||
@@ -292,7 +294,7 @@ export default function RulesTestList() {
|
|||||||
render: (_: unknown, record: RuleRow) => (
|
render: (_: unknown, record: RuleRow) => (
|
||||||
<div className="rule-name">
|
<div className="rule-name">
|
||||||
<strong>{record.name}</strong>
|
<strong>{record.name}</strong>
|
||||||
<span>{record.ruleId}</span>
|
<span>{record.isPlaceholder ? record.description : record.ruleId}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -320,7 +322,9 @@ export default function RulesTestList() {
|
|||||||
width: '8%',
|
width: '8%',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
render: (_: unknown, record: RuleRow) => (
|
render: (_: unknown, record: RuleRow) => (
|
||||||
<Tag color={riskColor(record.risk)} size="sm">{record.risk}</Tag>
|
<Tag color={record.isPlaceholder ? (record.yamlStatus === 'missing' ? 'orange' : 'blue') : riskColor(record.risk)} size="sm">
|
||||||
|
{record.isPlaceholder ? (record.yamlStatus === 'missing' ? '待修复' : '待配置') : record.risk}
|
||||||
|
</Tag>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -329,7 +333,7 @@ export default function RulesTestList() {
|
|||||||
width: '8%',
|
width: '8%',
|
||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
render: (_: unknown, record: RuleRow) => (
|
render: (_: unknown, record: RuleRow) => (
|
||||||
<Tag color="gray" size="sm">{record.score}</Tag>
|
<Tag color="gray" size="sm">{record.isPlaceholder ? '-' : record.score}</Tag>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -337,7 +341,7 @@ export default function RulesTestList() {
|
|||||||
key: 'dependencies',
|
key: 'dependencies',
|
||||||
width: '20%',
|
width: '20%',
|
||||||
render: (_: unknown, record: RuleRow) => (
|
render: (_: unknown, record: RuleRow) => (
|
||||||
<span>{record.dependencies.length > 0 ? record.dependencies.slice(0, 3).join('、') : '-'}</span>
|
<span>{record.isPlaceholder ? '先进入配置页补规则与依赖' : (record.dependencies.length > 0 ? record.dependencies.slice(0, 3).join('、') : '-')}</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -347,7 +351,7 @@ export default function RulesTestList() {
|
|||||||
align: 'center' as const,
|
align: 'center' as const,
|
||||||
render: (_: unknown, record: RuleRow) => (
|
render: (_: unknown, record: RuleRow) => (
|
||||||
<Link className="operation-btn" to={`/rulesTest/detail?packId=${encodeURIComponent(record.packId)}&ruleId=${encodeURIComponent(record.ruleId || record.id)}`}>
|
<Link className="operation-btn" to={`/rulesTest/detail?packId=${encodeURIComponent(record.packId)}&ruleId=${encodeURIComponent(record.ruleId || record.id)}`}>
|
||||||
<i className="ri-settings-3-line"></i> 配置
|
<i className="ri-settings-3-line"></i> {record.isPlaceholder ? '去配置' : '配置'}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,7 +184,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rules-test-page .rules-test-table td {
|
.rules-test-page .rules-test-table td {
|
||||||
|
padding-top: 18px;
|
||||||
|
padding-bottom: 18px;
|
||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rules-test-page .rules-test-table td:nth-child(2),
|
.rules-test-page .rules-test-table td:nth-child(2),
|
||||||
@@ -213,7 +216,9 @@
|
|||||||
.rules-test-page .rule-name {
|
.rules-test-page .rule-name {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
|
min-height: 54px;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rules-test-page .rule-name strong {
|
.rules-test-page .rule-name strong {
|
||||||
@@ -230,7 +235,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
|
min-height: 54px;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rules-test-page .rule-check-type span:last-child {
|
.rules-test-page .rule-check-type span:last-child {
|
||||||
|
|||||||
@@ -179,6 +179,15 @@ export function validateEditableRuleConfig(config: EditableRuleConfig): Validati
|
|||||||
message: '字段类型不能为空。'
|
message: '字段类型不能为空。'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (field.type === 'enum' && (!Array.isArray(field.allowed) || field.allowed.length === 0)) {
|
||||||
|
issues.push({
|
||||||
|
id: `field-allowed-${field.id}`,
|
||||||
|
severity: 'error',
|
||||||
|
area: '抽取配置',
|
||||||
|
target: field.name || '未命名字段',
|
||||||
|
message: '枚举字段必须配置可选值。'
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
config.subDocuments.forEach(document => {
|
config.subDocuments.forEach(document => {
|
||||||
@@ -219,6 +228,15 @@ export function validateEditableRuleConfig(config: EditableRuleConfig): Validati
|
|||||||
message: '文书字段类型不能为空。'
|
message: '文书字段类型不能为空。'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (field.type === 'enum' && (!Array.isArray(field.allowed) || field.allowed.length === 0)) {
|
||||||
|
issues.push({
|
||||||
|
id: `document-field-allowed-${document.id}-${field.id}`,
|
||||||
|
severity: 'error',
|
||||||
|
area: '案卷文书',
|
||||||
|
target: field.name || '未命名字段',
|
||||||
|
message: '文书枚举字段必须配置可选值。'
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -318,6 +336,7 @@ function rewriteExtractNodes(fields: ExtractFieldSummary[]): Array<Record<string
|
|||||||
fields: items.map((field) => ({
|
fields: items.map((field) => ({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
type: field.multipleEntities ? 'multi_entity' : field.type || 'verbatim',
|
type: field.multipleEntities ? 'multi_entity' : field.type || 'verbatim',
|
||||||
|
...(field.type === 'enum' && Array.isArray(field.allowed) && field.allowed.length > 0 ? { allowed: [...field.allowed] } : {}),
|
||||||
required_from: field.requiredFrom || 'draft',
|
required_from: field.requiredFrom || 'draft',
|
||||||
desc: field.description || '',
|
desc: field.description || '',
|
||||||
})),
|
})),
|
||||||
@@ -334,6 +353,7 @@ function rewriteSubDocumentNodes(subDocuments: SubDocumentSummary[]): Array<Reco
|
|||||||
fields: fields.map((field) => ({
|
fields: fields.map((field) => ({
|
||||||
name: field.name,
|
name: field.name,
|
||||||
type: field.multipleEntities ? 'multi_entity' : field.type || 'verbatim',
|
type: field.multipleEntities ? 'multi_entity' : field.type || 'verbatim',
|
||||||
|
...(field.type === 'enum' && Array.isArray(field.allowed) && field.allowed.length > 0 ? { allowed: [...field.allowed] } : {}),
|
||||||
desc: field.description || '',
|
desc: field.description || '',
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
@@ -353,11 +373,21 @@ function rewriteVisualElementNodes(visualElements: VisualElementSummary[]): Reco
|
|||||||
name: item.name,
|
name: item.name,
|
||||||
required: normalizeBooleanText(item.required),
|
required: normalizeBooleanText(item.required),
|
||||||
};
|
};
|
||||||
if (item.signerRoles && item.signerRoles.length > 0) node.signer_roles = [...item.signerRoles];
|
if (item.requiredFrom) node.required_from = item.requiredFrom;
|
||||||
if (item.signatureTypes && item.signatureTypes.length > 0) node.signature_types = [...item.signatureTypes];
|
if (item.expectedMatchField) {
|
||||||
if (item.privateSealRestricted) node.private_seal_restricted = true;
|
node.expected_text_match = {
|
||||||
|
field: item.expectedMatchField,
|
||||||
|
...(item.expectedMatchAlternatives && item.expectedMatchAlternatives.length > 0
|
||||||
|
? { alternatives: [...item.expectedMatchAlternatives] }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (item.prompt) node.prompt = item.prompt;
|
||||||
|
|
||||||
if (item.type === '签名') {
|
if (item.type === '签名') {
|
||||||
|
if (item.signerRoles && item.signerRoles.length > 0) node.signer_roles = [...item.signerRoles];
|
||||||
|
if (item.signatureTypes && item.signatureTypes.length > 0) node.signature_types = [...item.signatureTypes];
|
||||||
|
if (item.privateSealRestricted) node.private_seal_restricted = true;
|
||||||
sections.signatures.push(node);
|
sections.signatures.push(node);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -365,6 +395,7 @@ function rewriteVisualElementNodes(visualElements: VisualElementSummary[]): Reco
|
|||||||
sections.cross_page_seals.push(node);
|
sections.cross_page_seals.push(node);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (item.signatureTypes && item.signatureTypes.length > 0) node.allowed_types = [...item.signatureTypes];
|
||||||
sections.seals.push(node);
|
sections.seals.push(node);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -584,6 +615,9 @@ export function buildYamlPreview(config: EditableRuleConfig): string {
|
|||||||
lines.push(
|
lines.push(
|
||||||
` - name: ${yamlValue(field.name)}`,
|
` - name: ${yamlValue(field.name)}`,
|
||||||
` type: ${yamlValue(field.multipleEntities ? 'multi_entity' : field.type)}`,
|
` type: ${yamlValue(field.multipleEntities ? 'multi_entity' : field.type)}`,
|
||||||
|
...(field.type === 'enum' && Array.isArray(field.allowed) && field.allowed.length > 0
|
||||||
|
? [` allowed: [${field.allowed.map((item) => yamlValue(item)).join(', ')}]`]
|
||||||
|
: []),
|
||||||
` desc: ${yamlValue(field.description)}`
|
` desc: ${yamlValue(field.description)}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -607,6 +641,9 @@ export function buildYamlPreview(config: EditableRuleConfig): string {
|
|||||||
lines.push(
|
lines.push(
|
||||||
` - name: ${yamlValue(field.name)}`,
|
` - name: ${yamlValue(field.name)}`,
|
||||||
` type: ${yamlValue(field.multipleEntities ? 'multi_entity' : field.type)}`,
|
` type: ${yamlValue(field.multipleEntities ? 'multi_entity' : field.type)}`,
|
||||||
|
...(field.type === 'enum' && Array.isArray(field.allowed) && field.allowed.length > 0
|
||||||
|
? [` allowed: [${field.allowed.map((item) => yamlValue(item)).join(', ')}]`]
|
||||||
|
: []),
|
||||||
` desc: ${yamlValue(field.description)}`
|
` desc: ${yamlValue(field.description)}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { getUserSession } from '~/api/login/auth.server';
|
|||||||
import {
|
import {
|
||||||
buildRuleYamlPack,
|
buildRuleYamlPack,
|
||||||
EMPTY_RULE_YAML,
|
EMPTY_RULE_YAML,
|
||||||
|
type RuleSummary,
|
||||||
type RuleYamlPack,
|
type RuleYamlPack,
|
||||||
} from './rules-yaml-mock.server';
|
} from './rules-yaml-mock.server';
|
||||||
|
|
||||||
@@ -28,6 +29,23 @@ type RuleConfigPackApi = {
|
|||||||
sourceStatus?: 'ready' | 'empty' | 'missing';
|
sourceStatus?: 'ready' | 'empty' | 'missing';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type RuleConfigPackSummary = {
|
||||||
|
id: string;
|
||||||
|
documentType: string;
|
||||||
|
moduleType: string;
|
||||||
|
mainType: string;
|
||||||
|
subtype: string;
|
||||||
|
businessType: string;
|
||||||
|
ruleTypeCode: string;
|
||||||
|
currentVersionId?: number | null;
|
||||||
|
fallbackVersionId?: number | null;
|
||||||
|
resolvedVersionId?: number | null;
|
||||||
|
yamlName: string;
|
||||||
|
sourceStatus: 'ready' | 'empty' | 'missing';
|
||||||
|
rules: RuleSummary[];
|
||||||
|
};
|
||||||
|
|
||||||
type ApiEnvelope<T> = {
|
type ApiEnvelope<T> = {
|
||||||
code?: number;
|
code?: number;
|
||||||
message?: string;
|
message?: string;
|
||||||
@@ -52,6 +70,27 @@ function getMessage(payload: unknown, fallback: string): string {
|
|||||||
return String((payload as ApiEnvelope<unknown>).message || (payload as ApiEnvelope<unknown>).msg || fallback);
|
return String((payload as ApiEnvelope<unknown>).message || (payload as ApiEnvelope<unknown>).msg || fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function mapApiPackSummary(item: RuleConfigPackApi & { yamlName?: string; rules?: RuleSummary[] }): RuleConfigPackSummary {
|
||||||
|
const ruleTypeCode = String(item.ruleType || '').trim();
|
||||||
|
const businessType = item.mainType || item.documentType || '';
|
||||||
|
return {
|
||||||
|
id: String(item.packId),
|
||||||
|
documentType: item.documentType || '',
|
||||||
|
moduleType: item.moduleType || (item.documentType ? `${item.documentType}评查` : '规则配置'),
|
||||||
|
mainType: item.mainType || item.documentType || '',
|
||||||
|
subtype: item.subtype || '通用',
|
||||||
|
businessType,
|
||||||
|
ruleTypeCode,
|
||||||
|
currentVersionId: item.currentVersionId ?? null,
|
||||||
|
fallbackVersionId: item.fallbackVersionId ?? null,
|
||||||
|
resolvedVersionId: item.resolvedVersionId ?? null,
|
||||||
|
yamlName: String(item.yamlName || item.ruleName || ''),
|
||||||
|
sourceStatus: item.sourceStatus || 'empty',
|
||||||
|
rules: Array.isArray(item.rules) ? item.rules : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function mapApiPackToRuleYamlPack(item: RuleConfigPackApi): RuleYamlPack {
|
function mapApiPackToRuleYamlPack(item: RuleConfigPackApi): RuleYamlPack {
|
||||||
const ruleTypeCode = String(item.ruleType || '').trim();
|
const ruleTypeCode = String(item.ruleType || '').trim();
|
||||||
// 业务类型必须以后端 pack 聚合返回的 mainType 为准。
|
// 业务类型必须以后端 pack 聚合返回的 mainType 为准。
|
||||||
@@ -101,6 +140,12 @@ async function fetchRuleConfigPayload<T>(request: Request, path: string): Promis
|
|||||||
return payload.data;
|
return payload.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function loadRuleConfigPackSummaries(request: Request): Promise<RuleConfigPackSummary[]> {
|
||||||
|
const items = await fetchRuleConfigPayload<Array<RuleConfigPackApi & { yamlName?: string; rules?: RuleSummary[] }>>(request, '/api/v3/rule-config-packs?summaryOnly=true');
|
||||||
|
return items.map(mapApiPackSummary);
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadRuleConfigPacks(request: Request): Promise<RuleYamlPack[]> {
|
export async function loadRuleConfigPacks(request: Request): Promise<RuleYamlPack[]> {
|
||||||
const items = await fetchRuleConfigPayload<RuleConfigPackApi[]>(request, '/api/v3/rule-config-packs');
|
const items = await fetchRuleConfigPayload<RuleConfigPackApi[]>(request, '/api/v3/rule-config-packs');
|
||||||
return items.map(mapApiPackToRuleYamlPack);
|
return items.map(mapApiPackToRuleYamlPack);
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export type ExtractFieldSummary = {
|
|||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
multipleEntities: boolean;
|
multipleEntities: boolean;
|
||||||
|
allowed?: string[];
|
||||||
requiredFrom: string;
|
requiredFrom: string;
|
||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
@@ -89,9 +90,13 @@ export type RuleYamlPack = RulePackScope & {
|
|||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
required: string;
|
required: string;
|
||||||
|
requiredFrom?: string;
|
||||||
signerRoles?: string[];
|
signerRoles?: string[];
|
||||||
signatureTypes?: string[];
|
signatureTypes?: string[];
|
||||||
privateSealRestricted?: boolean;
|
privateSealRestricted?: boolean;
|
||||||
|
expectedMatchField?: string;
|
||||||
|
expectedMatchAlternatives?: string[];
|
||||||
|
prompt?: string;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -144,6 +149,13 @@ function stripYamlValue(value = ''): string {
|
|||||||
return value.trim().replace(/^['"]|['"]$/g, '').replace(/\u0000/g, '');
|
return value.trim().replace(/^['"]|['"]$/g, '').replace(/\u0000/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toStringList(value: unknown): string[] {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return value.map((item) => String(item || '').trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
function parseScalar(section: string, key: string): string {
|
function parseScalar(section: string, key: string): string {
|
||||||
const match = section.match(new RegExp(`^\\s{2}${key}:\\s*(.*)$`, 'm'));
|
const match = section.match(new RegExp(`^\\s{2}${key}:\\s*(.*)$`, 'm'));
|
||||||
return stripYamlValue(match?.[1] || '');
|
return stripYamlValue(match?.[1] || '');
|
||||||
@@ -400,38 +412,58 @@ export function parseRuleSummariesFromYaml(source: string): RuleSummary[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseTopLevelFields(source: string): ExtractFieldSummary[] {
|
function parseTopLevelFields(source: string): ExtractFieldSummary[] {
|
||||||
const section = getTopLevelSection(source, 'extract');
|
const parsed = YAML.parse(source || '') as Record<string, unknown> | null;
|
||||||
const extractedFields = splitBlocks(section, /^\s*-\s+group:\s*/).flatMap(groupBlock => {
|
const extractGroups = Array.isArray(parsed?.extract) ? parsed.extract : [];
|
||||||
const group = stripYamlValue(groupBlock.match(/^\s*-\s+group:\s*(.+)$/m)?.[1] || '未分组');
|
const extractedFields = extractGroups.flatMap((groupNode) => {
|
||||||
return splitBlocks(groupBlock, /^\s*-\s+name:\s*/).flatMap(fieldBlock => {
|
if (!groupNode || typeof groupNode !== 'object') {
|
||||||
const name = stripYamlValue(fieldBlock.match(/^\s*-\s+name:\s*(.+)$/m)?.[1] || '');
|
return [];
|
||||||
const rawType = stripYamlValue(fieldBlock.match(/^\s+type:\s*(.+)$/m)?.[1] || '-');
|
}
|
||||||
const parentField = {
|
const groupObject = groupNode as Record<string, unknown>;
|
||||||
|
const group = String(groupObject.group || '未分组').trim() || '未分组';
|
||||||
|
const fields = Array.isArray(groupObject.fields) ? groupObject.fields : [];
|
||||||
|
return fields.flatMap((fieldNode) => {
|
||||||
|
if (!fieldNode || typeof fieldNode !== 'object') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const field = fieldNode as Record<string, unknown>;
|
||||||
|
const name = String(field.name || '').trim();
|
||||||
|
if (!name) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const rawType = String(field.type || '-').trim();
|
||||||
|
const requiredFrom = String(field.required_from || '-').trim() || '-';
|
||||||
|
const parentField: ExtractFieldSummary = {
|
||||||
id: `${group}-${name}`,
|
id: `${group}-${name}`,
|
||||||
group,
|
group,
|
||||||
name,
|
name,
|
||||||
type: rawType === 'multi_entity' ? 'verbatim' : rawType,
|
type: rawType === 'multi_entity' ? 'verbatim' : rawType,
|
||||||
multipleEntities: rawType === 'multi_entity',
|
multipleEntities: rawType === 'multi_entity',
|
||||||
requiredFrom: stripYamlValue(fieldBlock.match(/^\s+required_from:\s*(.+)$/m)?.[1] || '-'),
|
allowed: toStringList(field.allowed),
|
||||||
description: stripYamlValue(fieldBlock.match(/^\s+desc:\s*(.+)$/m)?.[1] || '')
|
requiredFrom,
|
||||||
|
description: String(field.desc || '').trim(),
|
||||||
};
|
};
|
||||||
const childFields = Array.from(fieldBlock.matchAll(/^\s{2,}-\s+name:\s*(.+)$/gm)).map(match => {
|
const childFields = Array.isArray(field.fields) ? field.fields : [];
|
||||||
const childName = stripYamlValue(match[1]);
|
const normalizedChildFields = childFields.flatMap((childNode) => {
|
||||||
const start = fieldBlock.indexOf(match[0]);
|
if (!childNode || typeof childNode !== 'object') {
|
||||||
const next = fieldBlock.slice(start + match[0].length).search(/^\s{2,}-\s+name:\s*/m);
|
return [];
|
||||||
const childBlock = next === -1 ? fieldBlock.slice(start) : fieldBlock.slice(start, start + match[0].length + next);
|
}
|
||||||
const childType = stripYamlValue(childBlock.match(/^\s+type:\s*(.+)$/m)?.[1] || 'verbatim');
|
const childField = childNode as Record<string, unknown>;
|
||||||
return {
|
const childName = String(childField.name || '').trim();
|
||||||
|
if (!childName) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [{
|
||||||
id: `${group}-${name}-${childName}`,
|
id: `${group}-${name}-${childName}`,
|
||||||
group,
|
group,
|
||||||
name: `${name}[*].${childName}`,
|
name: `${name}[*].${childName}`,
|
||||||
type: childType,
|
type: String(childField.type || 'verbatim').trim() || 'verbatim',
|
||||||
multipleEntities: false,
|
multipleEntities: false,
|
||||||
requiredFrom: stripYamlValue(childBlock.match(/^\s+required_from:\s*(.+)$/m)?.[1] || parentField.requiredFrom),
|
allowed: toStringList(childField.allowed),
|
||||||
description: stripYamlValue(childBlock.match(/^\s+desc:\s*(.+)$/m)?.[1] || `${name}的子字段`)
|
requiredFrom: String(childField.required_from || requiredFrom).trim() || requiredFrom,
|
||||||
};
|
description: String(childField.desc || `${name}的子字段`).trim(),
|
||||||
|
}];
|
||||||
});
|
});
|
||||||
return [parentField, ...childFields];
|
return [parentField, ...normalizedChildFields];
|
||||||
});
|
});
|
||||||
}).filter(field => field.name);
|
}).filter(field => field.name);
|
||||||
|
|
||||||
@@ -493,6 +525,7 @@ function parseSubDocuments(source: string): SubDocumentSummary[] {
|
|||||||
name,
|
name,
|
||||||
type: rawType === 'multi_entity' ? 'verbatim' : rawType,
|
type: rawType === 'multi_entity' ? 'verbatim' : rawType,
|
||||||
multipleEntities: rawType === 'multi_entity',
|
multipleEntities: rawType === 'multi_entity',
|
||||||
|
allowed: toStringList(field.allowed),
|
||||||
requiredFrom: '-',
|
requiredFrom: '-',
|
||||||
description: String(field.desc || '').trim(),
|
description: String(field.desc || '').trim(),
|
||||||
}];
|
}];
|
||||||
@@ -553,14 +586,25 @@ function parseVisualElements(source: string): RuleYamlPack['visualElements'] {
|
|||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const expectedMatch = node.expected_text_match && typeof node.expected_text_match === 'object'
|
||||||
|
? (node.expected_text_match as Record<string, unknown>)
|
||||||
|
: null;
|
||||||
|
const signatureTypes = key === 'seals'
|
||||||
|
? toStringList(node.allowed_types)
|
||||||
|
: toStringList(node.signature_types);
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
id,
|
id,
|
||||||
name: String(node.name || id).trim(),
|
name: String(node.name || id).trim(),
|
||||||
type: label,
|
type: label,
|
||||||
required: String(node.required ?? '-').trim(),
|
required: String(node.required ?? '-').trim(),
|
||||||
signerRoles: toStringList(node.signer_roles),
|
requiredFrom: String(node.required_from ?? '').trim(),
|
||||||
signatureTypes: toStringList(node.signature_types),
|
signerRoles: key === 'signatures' ? toStringList(node.signer_roles) : [],
|
||||||
privateSealRestricted: Boolean(node.private_seal_restricted),
|
signatureTypes,
|
||||||
|
privateSealRestricted: key === 'signatures' ? Boolean(node.private_seal_restricted) : false,
|
||||||
|
expectedMatchField: String(expectedMatch?.field || '').trim(),
|
||||||
|
expectedMatchAlternatives: toStringList(expectedMatch?.alternatives),
|
||||||
|
prompt: String(node.prompt || '').trim(),
|
||||||
}];
|
}];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user