From ee4a38610252660fb80b7f5943e5f4fd71563572 Mon Sep 17 00:00:00 2001 From: JY <1140938202@qq.com> Date: Thu, 23 Oct 2025 19:33:18 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Config/Components/DataAudit/index.tsx | 213 +++++++++ .../Config/Nodes/RootNode/index.tsx | 2 + src/executor/tools/generate/thingTable.tsx | 22 + src/executor/tools/task/start/audit/index.tsx | 236 ++++++++++ src/executor/tools/task/start/default.tsx | 22 +- src/ts/base/model.ts | 12 + src/ts/core/work/audit/index.ts | 403 ++++++++++++++++++ src/ts/core/work/index.ts | 2 + src/utils/work.ts | 3 + 9 files changed, 914 insertions(+), 1 deletion(-) create mode 100644 src/components/Common/FlowDesign/Config/Components/DataAudit/index.tsx create mode 100644 src/executor/tools/task/start/audit/index.tsx create mode 100644 src/ts/core/work/audit/index.ts diff --git a/src/components/Common/FlowDesign/Config/Components/DataAudit/index.tsx b/src/components/Common/FlowDesign/Config/Components/DataAudit/index.tsx new file mode 100644 index 000000000..a9a7d74f9 --- /dev/null +++ b/src/components/Common/FlowDesign/Config/Components/DataAudit/index.tsx @@ -0,0 +1,213 @@ +import React, { useState } from 'react'; +import { Card, Divider, Button, Space, Popconfirm, Select } from 'antd'; +import OpenFileDialog from '@/components/OpenFileDialog'; +import { IForm, IReport, IWork } from '@/ts/core'; +import orgCtrl from '@/ts/controller'; +import { WorkNodeModel } from '@/ts/base/model'; +import { CheckBox } from 'devextreme-react'; + +interface IDataAuditConfigProps { + work: IWork; + current: WorkNodeModel; +} + +interface DataAuditItem { + id: string; + name: string; + bindFormId?: string; + allowNext: boolean; +} + +const DataAuditConfig: React.FC = ({ work, current }) => { + const [open, setOpen] = useState(false); + + const bindForms = + work.forms?.map((form: IForm | IReport) => ({ + id: form.id, + name: form.name, + })) || []; + + const [formData, setFormData] = useState(() => { + if (current?.dataAudits) { + return current.dataAudits.map((item: any) => ({ + id: item.formId, + name: item.formName || '', + bindFormId: item.bindFormId || undefined, + allowNext: item.allowNext ?? false, + })); + } + return []; + }); + + const updateFormData = (data: DataAuditItem[]) => { + setFormData([...data]); + if (!current?.dataAudits) { + current.dataAudits = []; + } + current.dataAudits = data.map((item) => ({ + formId: item.id, + formName: item.name, + bindFormId: item.bindFormId, + allowNext: item.allowNext, + })); + }; + + const addForm = (form: IForm) => { + const updatedData = [ + ...formData, + { + id: form.id, + name: form.name, + bindFormId: undefined, + allowNext: false, + }, + ]; + + updateFormData(updatedData); + }; + + // 删除表单 + const removeForm = (index: number) => { + const updatedData = [...formData]; + updatedData.splice(index, 1); + updateFormData(updatedData); + }; + + const handleBindFormChange = (index: number, bindFormId: string) => { + const updatedData = [...formData]; + updatedData[index] = { + ...updatedData[index], + bindFormId: bindFormId || undefined, + }; + updateFormData(updatedData); + }; + + // 新增:处理单个表单的allowNext切换 + const handleFormAllowNextToggle = (index: number, checked: boolean) => { + const updatedData = [...formData]; + updatedData[index] = { + ...updatedData[index], + allowNext: checked, + }; + updateFormData(updatedData); + }; + + return ( + <> + + + 数据合规校验 + + } + bodyStyle={{ padding: 12 }} + extra={ + + { + setOpen(true); + }}> + 添加 + + + }> + {formData.map((form, index) => ( +
+
+
+ {form.name} +
+
+
+ +
+
+ 允许跳过校验 + { + handleFormAllowNextToggle(index, value); + }} + /> +
+
+
+ removeForm(index)} + okText="确定" + cancelText="取消"> + + +
+
+
+ ))} +
+ + {open && ( + setOpen(false)} + onOk={(files) => { + if (files.length > 0) { + const form = files[0] as IForm; + addForm(form); + } + setOpen(false); + }} + /> + )} + + ); +}; + +export default DataAuditConfig; diff --git a/src/components/Common/FlowDesign/Config/Nodes/RootNode/index.tsx b/src/components/Common/FlowDesign/Config/Nodes/RootNode/index.tsx index 5120dfda5..398f489b7 100644 --- a/src/components/Common/FlowDesign/Config/Nodes/RootNode/index.tsx +++ b/src/components/Common/FlowDesign/Config/Nodes/RootNode/index.tsx @@ -17,6 +17,7 @@ import useAsyncLoad from '@/hooks/useAsyncLoad'; import orgCtrl from '@/ts/controller'; import FormBinding from '../../Components/FormBinding'; import DocumentConfig from '../../Components/Document'; +import DataAuditConfig from '../../Components/DataAudit'; interface IProps { work: IWork; @@ -155,6 +156,7 @@ const RootNode: React.FC = (props) => { detailForms={props.current.detailForms} /> + ReactNode; onRevertValue?: (id: string, field: string, value: any) => void; localization?: XThing[]; + showContent?: boolean; } /** 使用form生成表单 */ @@ -70,6 +72,25 @@ const GenerateThingTable = (props: IProps) => { return texts.join('/'); // 用 '/' 分隔返回的文本 }; + const renderErrorInfo = (data: schema.XThing) => { + const errors = data.data?.errInfo || []; + + if (errors.length === 0) { + return
无错误信息
; + } + return ( +
+ {errors.map((error: any, index: number) => ( +
+ + {index + 1}. {error.field || '未知字段'}:{' '} + {error.message || error.errMsg || '未知错误'} + +
+ ))} +
+ ); + }; return ( keyExpr="id" @@ -498,6 +519,7 @@ const GenerateThingTable = (props: IProps) => { ); }}> )} + {props.showContent && } {props.select && } void; + onStagging?: (instanceId: string) => void; + staggingId?: string; + content?: string; + children?: ReactElement; + splitDetailFormId?: string; + curTreeNode?: AssignTaskView; +} + +const auditConfig: React.FC = ({ apply, work, splitDetailFormId, finished }) => { + const [loadingBatch, setLoadingBatch] = useState(false); + const [activeTabKey, setActiveTabKey] = useState(''); + const [progress, setProgress] = useState(0); + const [dataLoaded, setDataLoaded] = useState(false); + const [showEmptyTable, setShowEmptyTable] = useState(true); // 控制是否显示空表 + const [isSkip, setIsSkip] = useState(false); + const service = useRef( + new WorkFormService(apply.target, apply.instanceData, true, apply.assignTask), + ); + const validator = new DataAuditValidator(apply.target, apply.instanceData); + service.current.setExtraParams((work as any)?.extraParams); + + const formStoresRef = useRef>(new Map()); + + const rawDataRef = useRef>(new Map()); + + const loadedToStoreRef = useRef>(new Set()); + + useEffect(() => { + const formStores = new Map(); + apply.instanceData.node.detailForms.forEach((form) => { + formStores.set( + form.id, + new ArrayStore({ + key: 'id', + data: [], + }), + ); + }); + formStoresRef.current = formStores; + + if (apply.instanceData.node.detailForms.length > 0) { + const initialActiveKey = + splitDetailFormId || apply.instanceData.node.detailForms[0].id; + setActiveTabKey(initialActiveKey); + } + }, [apply.instanceData, splitDetailFormId]); + + const handleBatchLoad = async () => { + for (const element of apply.instanceData!.node.forms) { + element.isAutoAll = true; + } + + if (apply.instanceData.node.dataAudits.length > 0) { + setLoadingBatch(true); + setDataLoaded(false); + setShowEmptyTable(true); + setProgress(0); + + // 清空数据 + rawDataRef.current.clear(); + loadedToStoreRef.current.clear(); + + try { + await validator.loadDataAudit((batchData: any) => { + const formId = batchData.formId; + + if (formId && batchData?.data) { + if (!rawDataRef.current.has(formId)) { + rawDataRef.current.set(formId, []); + } + const currentData = rawDataRef.current.get(formId) || []; + rawDataRef.current.set(formId, [...currentData, ...batchData.data]); + } + setProgress(validator.getProgress()); + }); + message.success('所有数据加载完成'); + setIsSkip(validator.isSkippable()); + setDataLoaded(true); + setShowEmptyTable(false); + + if (activeTabKey) { + loadFormDataToStore(activeTabKey); + } + } catch (error) { + message.error('数据加载过程中发生错误'); + console.error(error); + } finally { + setLoadingBatch(false); + setProgress(100); + } + } + }; + + const loadFormDataToStore = (formId: string) => { + const store = formStoresRef.current.get(formId); + const rawData = rawDataRef.current.get(formId); + if (store && rawData) { + const newStore = new ArrayStore({ + key: 'id', + data: rawData, + }); + + formStoresRef.current.set(formId, newStore); + } + }; + + // 获取表单字段 + const loadFields = (formId: string) => { + return service.current.model.fields[formId] || []; + }; + + // 渲染单个表格组件 + const renderTable = React.useCallback( + (form: schema.XForm) => { + const fields = loadFields(form.id); + + return ( + console.log('selection changed', e)} + toolbar={{ + visible: true, + items: [], + }} + dataSource={showEmptyTable ? [] : formStoresRef.current.get(form.id) || []} + /> + ); + }, + [service, activeTabKey, showEmptyTable], + ); + + // 加载所有表格项 + const loadTabItems = () => { + const items = []; + const detailForms = splitDetailFormId + ? apply.instanceData.node.detailForms.filter((i) => i.id === splitDetailFormId) + : apply.instanceData.node.detailForms; + + for (const form of detailForms) { + if ( + service.current.model.rules?.find( + (a) => a.destId == form.id && a.typeName == 'visible' && !a.value, + ) + ) { + continue; + } + items.push({ + key: form.id, + forceRender: false, + label: form.name, + children: renderTable(form), + }); + } + return items; + }; + + const handleTabChange = (key: string) => { + setActiveTabKey(key); + loadFormDataToStore(key); + }; + return ( + <> + {apply.instanceData.node.detailForms.length > 0 && ( + + )} + +
+ {isSkip && ( + + )} + +
+ + ); +}; + +export default auditConfig; diff --git a/src/executor/tools/task/start/default.tsx b/src/executor/tools/task/start/default.tsx index 4f5247547..f69edd4e3 100644 --- a/src/executor/tools/task/start/default.tsx +++ b/src/executor/tools/task/start/default.tsx @@ -45,7 +45,7 @@ import { IAssignTask } from '@/ts/core/work/assign/assign'; import { kernel } from '@/ts/base'; import { Instance } from '@/ts/core/work/instance'; import { delay } from '@/ts/base/common/timer'; - +import AuditConfig from './audit'; const pattern = /^T\d+$/; // 卡片渲染 @@ -77,6 +77,9 @@ const DefaultWayStart: React.FC = ({ splitDetailFormId, curTreeNode, }) => { + const [showAuditConfig, setShowAuditConfig] = useState( + apply.instanceData.node.dataAudits.length > 0, + ); const gatewayData = useRef(new Map()); const [openGatewayModal, setOpenGatewayModal] = useState(); const [gatewayFields, setGatewayFields] = useState([]); @@ -571,6 +574,23 @@ const DefaultWayStart: React.FC = ({ window.removeEventListener('submit-form', handleCustomEvent); }; }, []); + if (showAuditConfig) { + return ( +
+ { + setShowAuditConfig(false); + }} + /> +
+ ); + } return (
; +}; + +export class DataAuditValidator { + target: ITarget; + instanceData: model.InstanceDataModel; + // 添加状态属性 + private loaded: boolean = false; + private loading: boolean = false; + private progress: number = 0; + private skippable: boolean = false; + private formErrors: Map = new Map(); // 记录每个表单是否有错误 + private totalRecords: number = 0; // 总记录数 + private processedRecords: number = 0; // 已处理记录数 + + constructor(target: ITarget, instanceData: model.InstanceDataModel) { + this.target = target; + this.instanceData = instanceData; + } + + // 获取当前进度 + getProgress(): number { + return this.progress; + } + + // 是否已经加载过 + isLoaded(): boolean { + return this.loaded; + } + + // 是否正在加载 + isLoading(): boolean { + return this.loading; + } + + // 是否可以跳过 + isSkippable(): boolean { + return this.skippable; + } + + /** + * 分批加载数据 + */ + private async loadData( + metaForm: IForm, + take: number, + lastId: string, + ): Promise<{ data: any[]; totalCount: number }> { + const loadOptions = { + take: take, + userData: [], + filter: metaForm.parseFilter([]), + match: metaForm.parseClassify(), + requireTotalCount: true, + isCountQuery: false, + sort: [{ desc: false, selector: 'id' }], + options: { + match: { id: { _gt_: lastId } }, + }, + }; + let res = await metaForm.loadThing(loadOptions); + + return { + data: res.data, + totalCount: res.totalCount || 0, + }; + } + + /** + * 计算总记录数 + */ + private async calculateTotalRecords(): Promise { + let total = 0; + + if (!this.instanceData.node) return total; + + const resource = this.instanceData.node; + + for (const dataAudit of resource.dataAudits) { + try { + var target = orgCtrl.targets.find((a) => a.id == this.target.metadata.belongId); + if (target && dataAudit.bindFormId) { + let bindForms = await target.resource.formColl.find([dataAudit.bindFormId]); + + if (bindForms && bindForms.length > 0) { + let bindForm = new Form({ ...bindForms[0] }, target.directory); + + // 获取该表单的总记录数 + const result = await this.loadData(bindForm, 1, ''); + total += result.totalCount; + } + } + } catch (error) { + console.error('计算总记录数时出错:', error); + } + } + + return total; + } + + /** + * 更新跳过状态 + */ + private updateSkippableStatus(): void { + if (!this.instanceData.node) return; + + const resource = this.instanceData.node; + let canSkip = true; + + for (const dataAudit of resource.dataAudits) { + // 如果allowNext为true,可以跳过 + if (dataAudit.allowNext) { + continue; + } + + // 如果allowNext为false,必须检查该表单是否有错误 + const hasError = this.formErrors.get(dataAudit.bindFormId || '') || false; + if (hasError) { + canSkip = false; + break; + } + } + + this.skippable = canSkip; + } + + /** + * 加载并校验数据 + */ + async loadDataAudit( + onBatchLoaded?: (batchData: DataAuditResult) => void, + ): Promise { + // 如果已经加载过且不是强制重新加载,则直接返回 + if (this.loaded && !this.loading) { + return { + data: [], + errorInfo: new Map(), + }; + } + + // 如果正在加载,则直接返回 + if (this.loading) { + return { + data: [], + errorInfo: new Map(), + }; + } + + this.loading = true; + this.progress = 0; + this.processedRecords = 0; + + try { + // 先计算总记录数 + this.totalRecords = await this.calculateTotalRecords(); + if (this.instanceData.node) { + let resource = this.instanceData.node; + const allValidations: XValidation[] = []; + let hasError = false; + const totalAudits = resource.dataAudits.length; + + for (let auditIndex = 0; auditIndex < totalAudits; auditIndex++) { + const dataAudit = resource.dataAudits[auditIndex]; + + try { + var target = orgCtrl.targets.find( + (a) => a.id == this.target.metadata.belongId, + ); + if (target && dataAudit.bindFormId) { + let forms = await target.resource.formColl.find([dataAudit.formId]); + let bindForms = await target.resource.formColl.find([dataAudit.bindFormId]); + + if (forms && forms.length > 0) { + let newForm = new Form({ ...forms[0] }, target.directory); + let bindForm = new Form({ ...bindForms[0] }, target.directory); + await newForm.loadFields(); + + let filed = this.instanceData.fields[dataAudit.bindFormId]; + if (!filed) { + continue; + } + let newFiled = newForm.fields; + + // 分批加载并校验数据 + const batchSize = 300; + let progress = true; + let lastId = ''; + let formHasError = false; // 标记当前表单是否有错误 + + while (progress) { + // 获取一批数据 + const batchResult = await this.loadData(bindForm, batchSize, lastId); + const batchData = batchResult.data.map( + (item: { [s: string]: string }) => { + let newItem: any = {}; + Object.entries(item).forEach(([_key, value]) => { + const pattern = /^T\d+$/; + if (pattern.test(_key)) { + _key = _key.replace('T', ''); + } + newItem[_key] = value; + }); + return newItem; + }, + ); + + if ((batchData && batchData.length > 0) || !Array.isArray(batchData)) { + // 更新已处理记录数和进度 + this.processedRecords += batchData.length; + if (this.totalRecords > 0) { + this.progress = Math.floor( + (this.processedRecords / this.totalRecords) * 100, + ); + } + let checkList = await this.validateDataAgainstFields( + batchData, + filed, + newFiled, + ); + + // 检查是否有错误 + if (checkList.length > 0) { + hasError = true; + formHasError = true; + } + allValidations.push(...checkList); + + if (onBatchLoaded) { + const batchResultData: DataAuditResult = { + data: [], + }; + + if (checkList.length > 0) { + const errorInfoMap = new Map(); + + checkList.forEach((validation) => { + if (validation.data && validation.data.id) { + const itemId = validation.data.id; + if (!errorInfoMap.has(itemId)) { + errorInfoMap.set(itemId, []); + } + errorInfoMap.get(itemId)?.push(...validation.data.errorInfo); + } + }); + + const errorDataItems: any[] = []; + checkList.forEach((validation) => { + if ( + validation.data && + validation.data.id && + validation.data.originalData + ) { + errorDataItems.push({ + ...validation.data.originalData, + errInfo: validation.data.errorInfo || [], + }); + } + }); + + batchResultData.errorInfo = errorInfoMap; + batchResultData.data = errorDataItems; + } + + if (batchResultData.data.length > 0) { + (batchResultData as any).formId = dataAudit.bindFormId; + } + + onBatchLoaded(batchResultData); + } + + lastId = batchData[batchData.length - 1]?.id; + } else { + progress = false; + break; + } + } + + this.formErrors.set(dataAudit.bindFormId, formHasError); + } + } + } catch (error) { + console.error('处理 dataAudit 时出错:', error); + } + } + + this.updateSkippableStatus(); + + this.progress = 100; + this.loaded = true; + } + + // 默认返回空数组 + return { + data: [], + errorInfo: new Map(), + }; + } finally { + this.loading = false; + } + } + + /** + * 校验数据与字段的匹配 + */ + async validateDataAgainstFields( + data: any[], + filed: any[], + newFiled: any[], + ): Promise { + const validations: XValidation[] = []; + const dataErrorsMap = new Map(); + + for (let i = 0; i < data.length; i++) { + const dataItem = data[i]; + const itemId = dataItem.id; + + // 初始化每个数据项的错误数组 + if (!dataErrorsMap.has(itemId)) { + dataErrorsMap.set(itemId, []); + } + + for (const newField of newFiled) { + const fieldOptions = newField.options || {}; + + const corrFiled = filed.find( + (f) => + (f.code && f.code === newField.code) || (f.info && f.info === newField.info), + ); + + if (fieldOptions.isRequired) { + if (!corrFiled) { + // 必填字段不存在 + dataErrorsMap.get(itemId)?.push({ + errorCode: `MISSING_FIELD_${newField.id}`, + errorLevel: 'error', + message: `必填字段缺失: ${newField.name || newField.code || newField.id}`, + field: newField.name || newField.code || newField.id, + }); + continue; + } + const dataField = dataItem[corrFiled.propId]; + if (dataField === undefined || dataField === null || dataField === '') { + // 必填值为空 + dataErrorsMap.get(itemId)?.push({ + errorCode: `EMPTY_FIELD_${corrFiled.id}`, + errorLevel: 'error', + message: `字段 ${corrFiled.name} 不能为空`, + field: corrFiled.name, + }); + } + } + + // 类型不匹配检查 + if (corrFiled && newField.valueType) { + if (corrFiled.valueType && corrFiled.valueType !== newField.valueType) { + dataErrorsMap.get(itemId)?.push({ + errorCode: `TYPE_MISMATCH_${corrFiled.id}`, + errorLevel: 'warning', + message: `${corrFiled.name} 类型不匹配 (期望: ${newField.valueType}, 实际: ${corrFiled.valueType})`, + field: corrFiled.name, + expectedType: newField.valueType, + actualType: corrFiled.valueType, + }); + } + } + } + } + + dataErrorsMap.forEach((errors, itemId) => { + if (errors.length > 0) { + // 找到原始数据项 + const originalData = data.find((item) => item.id === itemId); + + validations.push({ + typeName: '校验信息', + errorCode: 'DATA_VALIDATION_ERRORS', + errorLevel: 'error', + message: `数据项 ${itemId} 存在 ${errors.length} 个校验错误`, + position: `ID: ${itemId}`, + reason: '', + files: [], + instanceId: '', + data: { + id: itemId, + errorInfo: errors, + originalData: originalData, + }, + } as any); + } + }); + + return validations; + } +} diff --git a/src/ts/core/work/index.ts b/src/ts/core/work/index.ts index dd4329758..26aef3a3a 100644 --- a/src/ts/core/work/index.ts +++ b/src/ts/core/work/index.ts @@ -693,12 +693,14 @@ export class Work extends Version implements IWork { node.primaryForms = resource; node.formRules = []; node.executors = []; + node.dataAudits = []; } else { node.primaryForms = resource.primaryForms ?? []; node.detailForms = resource.detailForms ?? []; node.forms = resource.forms ?? []; node.print = resource.print; node.formRules = resource.formRules; + node.dataAudits = resource.dataAudits; node.executors = resource.executors; node.buttons = resource.buttons; node.documentConfig = resource.documentConfig || { diff --git a/src/utils/work.ts b/src/utils/work.ts index 1ce35eff2..7aa87d613 100644 --- a/src/utils/work.ts +++ b/src/utils/work.ts @@ -343,12 +343,14 @@ const convertNode = ( executors: resource.executors, encodes: resource.encodes, formRules: resource.formRules, + dataAudits: resource.dataAudits, destName: resource.destName, children: convertNode(resource.children, validation), branches: resource.branches?.map((a) => convertBranch(a, validation)), resource: JSON.stringify({ executors: resource.executors ?? [], formRules: resource.formRules ?? [], + dataAudits: resource.dataAudits ?? [], printData: resource.printData ?? {}, print: resource.print ?? [], buttons: resource.buttons ?? [], @@ -432,6 +434,7 @@ const loadResource = (resource: any, parentCode: string = ''): any => { detailForms: nodeResource.detailForms ?? resource.detailForms ?? [], executors: nodeResource.executors ?? [], formRules: nodeResource.formRules ?? [], + dataAudits: nodeResource.dataAudits ?? [], printData: nodeResource.printData ?? { attributes: [], type: '', -- Gitee From 7b71aeb09eb2595207390159d691a1571d7c00ef Mon Sep 17 00:00:00 2001 From: JY <1140938202@qq.com> Date: Fri, 24 Oct 2025 09:25:18 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/executor/tools/task/start/default.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/executor/tools/task/start/default.tsx b/src/executor/tools/task/start/default.tsx index f69edd4e3..b870c5773 100644 --- a/src/executor/tools/task/start/default.tsx +++ b/src/executor/tools/task/start/default.tsx @@ -78,7 +78,7 @@ const DefaultWayStart: React.FC = ({ curTreeNode, }) => { const [showAuditConfig, setShowAuditConfig] = useState( - apply.instanceData.node.dataAudits.length > 0, + apply.instanceData.node.dataAudits?.length > 0, ); const gatewayData = useRef(new Map()); const [openGatewayModal, setOpenGatewayModal] = useState(); -- Gitee