From 5d3c11b7f2ad3ad0c536d2135fed052ed87d6a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=BD=B3=E9=91=AB?= <695997094@qq.com> Date: Mon, 26 Aug 2024 10:17:33 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E6=9A=82=E5=AD=98=E6=B5=AE=E5=8A=A8?= =?UTF-8?q?=E8=A1=8C=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DataStandard/ReportForm/Utils/index.tsx | 326 ++++++++++ .../DataStandard/ReportForm/Viewer/index.tsx | 598 ++++++++++-------- .../ReportForm/config/floatRows.tsx | 172 +++++ .../DataStandard/ReportForm/config/index.tsx | 11 + .../ReportForm/design/components/hotTable.tsx | 419 ++++++------ .../DataStandard/ReportForm/design/index.tsx | 12 +- .../DataStandard/ReportForm/index.tsx | 6 +- .../DataStandard/ReportForm/report/index.tsx | 7 +- .../DataStandard/ReportForm/types.ts | 44 ++ 9 files changed, 1098 insertions(+), 497 deletions(-) create mode 100644 src/components/DataStandard/ReportForm/config/floatRows.tsx diff --git a/src/components/DataStandard/ReportForm/Utils/index.tsx b/src/components/DataStandard/ReportForm/Utils/index.tsx index 490e1cfb0..3f580d8b0 100644 --- a/src/components/DataStandard/ReportForm/Utils/index.tsx +++ b/src/components/DataStandard/ReportForm/Utils/index.tsx @@ -1,3 +1,13 @@ +import { CellSettings } from 'handsontable/settings'; +import { CellInfo, ReportSettingInfo, XFloatRowsInfo } from '../types'; +import { kernel, model, schema } from '@/ts/base'; +import Handsontable from 'handsontable'; +import { textRenderer } from 'handsontable/renderers'; +import { NodeType } from '@/ts/base/enum'; +import { getWidget } from '../../WorkForm/Utils'; +import { orgAuth, IForm } from '@/ts/core'; +import { XAttribute } from '@/ts/base/schema'; + /** 数字转字母 */ export const numberToLetters = (number: any) => { let result = ''; @@ -99,3 +109,319 @@ export const converBorder = (border: any) => { return borderCss; }; + +// 根据选中区域获取坐标 +export const getRanges = (range: any) => { + let cellsRanges: any[] = []; + for (let y = range.yMin; y <= range.yMax; y++) { + for (let x = range.xMin; x <= range.xMax; x++) { + cellsRanges.push([x, y]); + } + } + return cellsRanges; +}; + +// 判断坐标是否属于该区域 +export const isPointInRange = (point: any, range: any) => { + const { row: x, col: y } = point; + return x >= range.xMin && x <= range.xMax && y >= range.yMin && y <= range.yMax; +}; + +// 获取选中单元格区域 +export const getSelected = (selected: number[] | undefined) => { + if (!selected) { + return { + xMin: 0, + yMin: 0, + xMax: 0, + yMax: 0, + unselected: true, + }; + } + /** 因为会从不同的方向选择,需要重新排序 */ + const xMin = Math.min(selected[0], selected[2]); + const xMax = Math.max(selected[0], selected[2]); + const yMin = Math.min(selected[1], selected[3]); + const yMax = Math.max(selected[1], selected[3]); + return { + xMin, + xMax, + yMin, + yMax, + }; +}; + +const getCellSetting = (cells: CellSettings[], row: number, col: number) => { + let cell = cells.find((c) => c.col == col && c.row == row); + if (!cell) { + cell = { col, row }; + } + return cell; +}; + +const setData = (data: any[][], row: number, col: number, value: any) => { + if (!data[row]) { + data[row] = []; + } + return (data[row][col] = value); +}; + +export const updataCells = ( + setting: ReportSettingInfo, + data: any[][], + attrMap: Dictionary, + fields: model.FieldModel[], + readonly: boolean | undefined, + reportStatus: model.ReceptionContentBase | null, +) => { + const cells: CellSettings[] = []; + const styleList: any[] = setting?.styleList || []; + const classList: any[] = setting?.classList || []; + + styleList.forEach((item: any) => { + const cell = getCellSetting(cells, item.row, item.col); + cell.renderer = ( + instance: Handsontable.Core, + TD: HTMLTableCellElement, + row: number, + col: number, + prop: string | number, + value: any, + cellProperties: Handsontable.CellProperties, + ) => { + textRenderer(instance, TD, row, col, prop, value, cellProperties); + const items = setting.styleList.find((it: any) => it.row === row && it.col === col); + if (items) { + for (let key in items.styles) { + if (key === 'paddingLeft') { + TD.style[key] = items.styles[key] + 'px'; + } else { + TD.style[key as any] = items.styles[key]; + } + } + } + }; + cells.push(cell); + }); + classList.forEach((item: any) => { + const cell = getCellSetting(cells, item.row, item.col); + cells.push(cell); + let arr = []; + for (let k in item.class) { + arr.push(item.class[k]); + } + cell.className = arr.join(' '); + }); + + const isSummary = reportStatus?.treeNode.nodeType == NodeType.Summary; + + setting.cells?.forEach((item: CellInfo) => { + const cell = getCellSetting(cells, item.row, item.col); + cells.push(cell); + const prop = attrMap[item.prop.id]; + + if (!readonly) { + cell.readOnly = prop.options?.readOnly; + } + cell.renderer = 'customStylesRenderer'; + if ((prop.rule && JSON.parse(prop.rule).id) || prop.options?.isComputed) { + cell.renderType = 'computed'; + } else { + cell.renderType = 'input'; + } + switch (getWidget(prop.valueType, prop.widget)) { + case '数字框': + cell.type = 'numeric'; + cell.numericFormat = { + pattern: { + // 传undefined也会报错,逆天 + // mantissa: prop.options?.accuracy, + thousandSeparated: true, + }, + culture: 'zh-CN', + }; + if (typeof prop.options?.accuracy === 'number') { + (cell.numericFormat.pattern as any).mantissa = prop.options?.accuracy; + } + } + fields.forEach((field) => { + if (prop.id === field.id) { + if (field.options!.readOnly && readonly) { + cell.readOnly = true; + } else if (!field.options?.readOnly && readonly) { + cell.readOnly = true; + } + if (field.options!.defaultValue) { + if (field.lookups!.length > 0) { + const items = field.lookups!.find( + (it: any) => it.value === field.options!.defaultValue, + ); + data = setData(data, item.row, item.col, items?.text); + } else { + data = setData(data, item.row, item.col, field.options!.defaultValue); + } + } + + if (isSummary && field.options?.isSummary) { + cell.readOnly = true; + cell.className = 'is-readonly'; + } + } + }); + + cells.push(cell); + }); + + return { + cells: cells, + data: data, + }; +}; + +export const refreshFormData = async ( + formData: model.FormEditData | undefined, + attrMap: Dictionary, + cellList: CellInfo[], + type: string, +) => { + const data: [number, number, any][] = []; + if (formData?.after) { + for (let key in formData.after[0]) { + for (let item of cellList) { + let value = formData?.after[0][key]; + let pass = false; + if (key === item.prop?.propId) { + pass = true; + } else if (key.includes('.')) { + const [leftPart, rightPart] = key.split('.'); + if (leftPart === type) { + const coords = rightPart.split(',').map((v) => parseInt(v)); + if (coords[0] == item.row && coords[1] == item.col) { + pass = true; + } + } + } else if (key.includes(',') && type === 'primary') { + const coords = key.split(',').map((v) => parseInt(v)); + if (coords[0] == item.row && coords[1] == item.col) { + pass = true; + } + } + if (pass) { + const prop = attrMap[item.prop.id]; + switch (getWidget(prop.valueType, prop.widget)) { + case '操作人': + case '操作组织': + if (value) { + let result = await kernel.queryEntityById({ id: value }); + if (result.success && result.data) { + value = result.data.name; + } + } + break; + } + data.push([item.row, item.col, value]); + } + } + } + } + + return data; +}; + +export const isSelectWidget = (widgetType: string): boolean => { + const specificWidgets = [ + '选择框', + '单选框', + '引用选择框', + '多级选择框', + '人员搜索框', + '单位搜索框', + '群组搜索框', + '组织群搜索框', + '成员选择框', + '内部机构选择框', + '日期选择框', + '时间选择框', + ]; + return specificWidgets.includes(widgetType); +}; + +export const generateProperties = async ( + metaForm: IForm, + floatSetting: XFloatRowsInfo, + tempCells: any[], + subDataLength?: number, +) => { + let arrs: any[] = []; + let subData: any[] = []; + let cells: any[] = [...tempCells]; + let dataLength = subDataLength || 0; + for (let i = 0; i < 10; i++) { + let rowData: any[] = []; + for (let k = 0; k < floatSetting.rowsInfo.length; k++) { + let findItemIndex = cells.findIndex((item: any) => { + return item.row === i + dataLength && item.col === k; + }); + if (floatSetting.rowsInfo[k].isLineNumber) { + rowData.push(i + dataLength + 1); + } else { + rowData.push(undefined); + } + const row = i + dataLength + 1; + const col = k + floatSetting.startColumn + 1; + const coord = numberToLetters(col) + floatSetting.floatStartLine + '-' + row; + const item: any = await metaForm.createReportTemporaryAttribute({ + code: coord, + isChangeSource: false, + isChangeTarget: false, + name: metaForm.name + '.floatRow' + '-' + coord, + remark: metaForm.name + '.floatRow' + '-' + coord, + valueType: floatSetting.rowsInfo[k].type, + }); + if (floatSetting.rowsInfo[k].applicationData) { + item.valueType = floatSetting.rowsInfo[k].applicationData.valueType; + item.speciesId = floatSetting.rowsInfo[k].applicationData.speciesId; + } + const newItem: XAttribute = { + propId: item.id, + property: item, + ...item, + rule: '{}', + options: { + visible: true, + isRequired: false, + readOnly: floatSetting.rowsInfo[k].isOnlyRead, + reportTemporaryCoord: floatSetting.coords, + }, + formId: metaForm.id, + authId: orgAuth.SuperAuthId, + }; + if (findItemIndex > -1) { + cells[findItemIndex] = { + col: k, + row: i + dataLength, + isFloatRow: true, + prop: newItem, + }; + } else { + arrs.push(newItem); + metaForm.metadata.attributes.push(newItem); + cells.push({ + col: k, + row: i + dataLength, + isFloatRow: true, + prop: newItem, + }); + } + } + subData.push(rowData); + } + const fields = await metaForm.loadFields(); + + return { + arrs: arrs, + subData: subData, + cells: cells, + fields: fields || [], + }; +}; diff --git a/src/components/DataStandard/ReportForm/Viewer/index.tsx b/src/components/DataStandard/ReportForm/Viewer/index.tsx index 387e4ee27..dc4bfb3d3 100644 --- a/src/components/DataStandard/ReportForm/Viewer/index.tsx +++ b/src/components/DataStandard/ReportForm/Viewer/index.tsx @@ -1,14 +1,12 @@ import { ReceptionContext } from '@/components/DataPreview/task'; import { kernel, model, schema } from '@/ts/base'; -import { NodeType } from '@/ts/base/enum'; import { IBelong } from '@/ts/core'; import { FormChangeEvent } from '@/ts/scripting/core/types/rule'; import { HotTable } from '@handsontable/react'; -import Handsontable from 'handsontable'; +import Handsontable from 'handsontable/base'; import 'handsontable/dist/handsontable.min.css'; import { registerLanguageDictionary, zhCN } from 'handsontable/i18n'; import { registerAllModules } from 'handsontable/registry'; -import { textRenderer } from 'handsontable/renderers'; import { CellSettings } from 'handsontable/settings'; import React, { ReactNode, @@ -19,23 +17,38 @@ import React, { useState, } from 'react'; import { getWidget, isDirectEditable } from '../../../DataStandard/WorkForm/Utils'; -import { CellInfo, ReportInfo, ReportSettingInfo, stagDataInfo } from '../types'; +import { + CellInfo, + ReportInfo, + ReportSettingInfo, + XFloatRowsInfo, + XRowsInfo, + stagDataInfo, +} from '../types'; import CellItem from './cellItem'; import WorkFormService from '@/ts/scripting/core/services/WorkFormService'; import Toolbar, { Item } from 'devextreme-react/toolbar'; import { EditModal } from '@/executor/tools/editModal'; -import { XAttribute, XThing } from '@/ts/base/schema'; +import { XAttribute } from '@/ts/base/schema'; import { CellChange, ChangeSource } from 'handsontable/common'; +import { Form } from '@/ts/core/thing/standard/form'; +import { + updataCells, + refreshFormData, + isSelectWidget, + generateProperties, +} from './../Utils'; import { ReportReception } from '@/ts/core/work/assign/reception/report'; import { useFixedCallback } from '@/hooks/useFixedCallback'; import { ReportStatus } from '@/ts/base/model'; import { Modal, Table, Tag, message } from 'antd'; import { formatNumber } from '@/utils'; +import { NodeType } from '@/ts/base/enum'; registerLanguageDictionary(zhCN); registerAllModules(); const WorkReportViewer: React.FC<{ - data: XThing; + data: { [key: string]: any }; allowEdit: boolean; belong: IBelong; form: schema.XForm; @@ -45,10 +58,13 @@ const WorkReportViewer: React.FC<{ fields: model.FieldModel[]; rules: model.RenderRule[]; formData?: model.FormEditData; + primary: { + [id: string]: any; + }; onValuesChange?: (fieldId: string, value: any, data: any, coord?: any) => void; activeTabKey?: string; height?: string; - service: WorkFormService; + service?: WorkFormService; }> = (props) => { props.data.name = props.form.name; const [editMode, setEditMode] = useState(false); @@ -58,9 +74,10 @@ const WorkReportViewer: React.FC<{ const [selectValue, setSelectValue] = useState(); const [cells, setCells] = useState([]); const hotRef = useRef<{ hotInstance: Handsontable }>(null!); + const [floatCoord, setFloatCoord] = useState(); const [ready, setReady] = useState(false); - + let metaForm = new Form(props.form, props.belong.directory); const [currentCell, setCurrentCell] = useState(null); const attrMap = useMemo(() => { @@ -87,11 +104,15 @@ const WorkReportViewer: React.FC<{ const isSummary = reportStatus?.treeNode.nodeType == NodeType.Summary; const onValueChange = (fieldId: string, value: any) => { + const hot = hotRef.current.hotInstance; if (coordinate?.row) { - setStagData([ - ...stagData, - { row: coordinate.row, col: coordinate.col, fieldId: fieldId, value: value }, - ]); + const cellMeta = hot.getCellMeta(coordinate.row, coordinate.col); + if (!cellMeta.subTable) { + setStagData([ + ...stagData, + { row: coordinate.row, col: coordinate.col, fieldId: fieldId, value: value }, + ]); + } } const checkHasChanged = (fieldId: string, value: any) => { if (value instanceof Object) { @@ -115,83 +136,41 @@ const WorkReportViewer: React.FC<{ }; const writeData = (text: string) => { const hot = hotRef.current.hotInstance; - hot.setDataAtCell(coordinate.row, coordinate.col, text, 'writeData'); - const coordinateId = [coordinate.row, coordinate.col].toString(); - props.onValuesChange?.apply(this, [coordinateId, text, props.data]); - }; - - function isAllDigits(str: string) { - return /^\d+$/.test(str); - } - - async function refreshFormData(cellList: CellInfo[]) { - const data: [number, number, any][] = []; - if (props.formData?.after) { - for (let key in props.formData.after[0]) { - for (let item of cellList) { - let value = props.formData?.after[0][key]; - let pass = false; - if (key === item.prop?.propId) { - pass = true; - } else if (key.includes(',')) { - const coords = key.split(',').map((v) => parseInt(v)); - if (coords[0] == item.row && coords[1] == item.col) { - pass = true; - } - } - if (pass) { - const prop = attrMap[item.prop.id]; - switch (getWidget(prop.valueType, prop.widget)) { - case '操作人': - case '操作组织': - if (value) { - if (isAllDigits(value)) { - let result = await kernel.queryEntityById({ id: value }); - if (result.success && result.data) { - value = result.data.name; - } - } - } - break; - } - data.push([item.row, item.col, value]); - } - } - } + const cellMeta = hot.getCellMeta(coordinate.row, coordinate.col); + if (cellMeta.subTable) { + const coordinateId = + cellMeta.floatSetting.coords + '.' + [floatCoord.row, floatCoord.col].toString(); + props.onValuesChange?.apply(this, [coordinateId, text, props.data]); + } else { + hot.setDataAtCell(coordinate.row, coordinate.col, text, 'writeData'); + const coordinateId = [coordinate.row, coordinate.col].toString(); + props.onValuesChange?.apply(this, [coordinateId, text, props.data]); } - return data; - } + }; const onBatchUpdate = useFixedCallback(async (changeEvents: FormChangeEvent[]) => { const hot = hotRef.current.hotInstance; let changeArray: [number, number, any][] = []; for (const item of changeEvents) { - let change: [number, number, any] | null = null; - let needText = false; - - let prop = cells.find((value) => { + props.primary[item.destId] = item.value; + } + for (const item of changeEvents) { + let prop = cells.find((value: any) => { return value.prop.id === item.destId; }); - if (!prop) { - continue; - } - - if (item.value || item.value == 0) { + if (prop && (item.value || item.value == 0)) { const field = fieldMap[prop.prop.id]; switch (field?.valueType) { case '选择型': case '分类型': { const value = field.lookups?.find((lookup) => lookup.value === item.value); - change = [prop.row, prop.col, value?.text]; - needText = true; + changeArray.push([prop.row, prop.col, value?.text]); break; } case '用户型': switch (field.widget) { case '操作人': case '操作组织': - // 自动生成的,不赋值 - continue; case '人员搜索框': case '单位搜索框': case '群组搜索框': @@ -199,33 +178,18 @@ const WorkReportViewer: React.FC<{ case '成员选择框': case '内部机构选择框': { const result = await kernel.queryEntityById({ id: item.value }); - change = [prop.row, prop.col, result.data?.name]; - needText = true; + changeArray.push([prop.row, prop.col, result.data?.name]); break; } } break; default: - change = [prop.row, prop.col, item.value]; + changeArray.push([prop.row, prop.col, item.value]); break; } } - - // 给表单赋值 - props.data[item.destId] = item.value; - // 居然没有地方通知更新,依赖Bug运行??? - onValueChange(item.destId, item.value); - - if (change) { - changeArray.push(change); - if (needText) { - const coordinateId = [prop.row, prop.col].join(','); - props.data[coordinateId] = change[2]; - } - } } - // 更新handsontable并阻止afterChange事件 - hot.setDataAtCell(changeArray, 'writeData'); + hot.setDataAtCell(changeArray); }); useEffect(() => { @@ -240,7 +204,7 @@ const WorkReportViewer: React.FC<{ const selectItem = Object.values(sheetListData)[0] as ReportInfo; const setting = selectItem?.data?.setting || {}; const datas = selectItem?.data?.data || [[]]; - updateHot(setting, datas); + updateHot(setting, datas, setting.floatRowsSetting || []); return () => { handles.forEach((dispose) => dispose()); @@ -249,7 +213,7 @@ const WorkReportViewer: React.FC<{ async function onCellsChange() { const hot = hotRef.current.hotInstance; - const changes = await refreshFormData(cells); + const changes = await refreshFormData(props.formData, attrMap, cells, 'primary'); hot.setDataAtCell(changes); } @@ -260,146 +224,37 @@ const WorkReportViewer: React.FC<{ onCellsChange(); }, [cells]); - const updateHot = async (setting: ReportSettingInfo, data: any[][]) => { + const updateHot = async ( + setting: ReportSettingInfo, + data: any[][], + floatRowsSetting: XFloatRowsInfo[], + ) => { const hot = hotRef.current.hotInstance; - const cells: CellSettings[] = []; - - function getCellSetting(row: number, col: number) { - let cell = cells.find((c) => c.col == col && c.row == row); - if (!cell) { - cell = { col, row }; - cells.push(cell); - } - return cell; - } - - function setData(row: number, col: number, value: any) { - if (!data[row]) { - data[row] = []; - } - data[row][col] = value; - } - - const styleList: any[] = setting.styleList || []; - const classList: any[] = setting.classList || []; - - styleList.forEach((item: any) => { - const cell = getCellSetting(item.row, item.col); - cell.renderer = ( - instance: Handsontable.Core, - TD: HTMLTableCellElement, - row: number, - col: number, - prop: string | number, - value: any, - cellProperties: Handsontable.CellProperties, - ) => { - textRenderer(instance, TD, row, col, prop, value, cellProperties); - const items = setting.styleList.find( - (it: any) => it.row === row && it.col === col, - ); - if (items) { - for (let key in items.styles) { - if (key === 'paddingLeft') { - TD.style[key] = items.styles[key] + 'px'; - } else { - TD.style[key as any] = items.styles[key]; - } - } - } - }; - }); - classList.forEach((item: any) => { - const cell = getCellSetting(item.row, item.col); - let arr = []; - for (let k in item.class) { - arr.push(item.class[k]); + const result = updataCells( + setting, + data, + attrMap, + props.fields, + props.readonly, + reportStatus, + ); + const cells: CellSettings[] = result.cells; + data = result.data; + + floatRowsSetting.forEach((infos) => { + if (infos.isFloatRows) { + const mergeCells = infos.mergeCells; + setting.mergeCells?.push({ + row: mergeCells.xMin, + col: mergeCells.yMin, + rowspan: 1, + colspan: + mergeCells.yMin > 0 + ? mergeCells.yMax - mergeCells.yMin + 1 + : mergeCells.yMax + 1, + removed: false, + }); } - cell.className = arr.join(' '); - }); - - setting.cells?.forEach((item: CellInfo) => { - const cell = getCellSetting(item.row, item.col); - const prop = attrMap[item.prop.id]; - - if (!props.readonly) { - cell.readOnly = prop.options?.readOnly; - } - cell.renderer = 'customStylesRenderer'; - if ((prop.rule && JSON.parse(prop.rule).id) || prop.options?.isComputed) { - cell.renderType = 'computed'; - } else { - cell.renderType = 'input'; - } - switch (getWidget(prop.valueType, prop.widget)) { - case '数字框': - cell.type = 'numeric'; - cell.numericFormat = { - pattern: { - // 传undefined也会报错,逆天 - // mantissa: prop.options?.accuracy, - thousandSeparated: true, - }, - culture: 'zh-CN', - }; - if (typeof prop.options?.accuracy === 'number') { - (cell.numericFormat.pattern as any).mantissa = prop.options?.accuracy; - } - } - switch (prop.valueType) { - case '选择型': - case '分类型': - case '用户型': - cell.validator = function (text, callback) { - const value = props.data[prop.id]; - if (!value && !text) { - return callback(true); - } else if (!value && text) { - if (prop.widget == '操作人' || prop.widget == '操作组织') { - return callback(true); - } - // 没有值但有中文,说明是手填的脏数据 - return callback(false); - } - if (typeof value !== 'string') { - return callback(false); - } - if (['选择型', '分类型'].includes(prop.valueType!) && /^S\d+$/.test(value)) { - return callback(true); - } else if (prop.valueType == '用户型' && /^\d+$/.test(value)) { - return callback(true); - } - // 手填的脏数据 - return callback(false); - }; - break; - default: - break; - } - props.fields.forEach((field) => { - if (prop.id === field.id) { - if (field.options!.readOnly && props.readonly) { - cell.readOnly = true; - } - if (field.options!.defaultValue) { - if (field.lookups!.length > 0) { - const items = field.lookups!.find( - (it: any) => it.value === field.options!.defaultValue, - ); - setData(item.row, item.col, items?.text); - } else { - setData(item.row, item.col, field.options!.defaultValue); - } - } - - if (isSummary && field.options?.isSummary) { - cell.readOnly = true; - cell.className = 'is-readonly'; - } - } - }); - - cells.push(cell); }); setCells(setting?.cells || []); @@ -413,63 +268,274 @@ const WorkReportViewer: React.FC<{ colWidths: setting.col_w, }); - const changes = await refreshFormData(setting?.cells || []); + hot.updateSettings({ + cell: cells, + }); + + const changes = await refreshFormData( + props.formData, + attrMap, + setting?.cells || [], + 'primary', + ); // 这种写法会报错,但下面的不会?? // hot.setDataAtCell(changes); hot.batch(() => { for (const change of changes) { hot.setDataAtCell(...change); } + floatRowsSetting.forEach((infos) => { + if (infos.isFloatRows) { + const mergeCells = infos.mergeCells; + hot.getCellMeta(mergeCells.xMin, mergeCells.yMin).floatSetting = infos; + hot.getCellMeta(mergeCells.xMin, mergeCells.yMin).renderer = customRenderer; + } + }); }); - hot.updateSettings({ - cell: cells, - }); - + // console.log(floatRowsSetting, 'floatRowsSettingfloatRowsSetting'); setTimeout(() => { setReady(true); }, 20); }; + const customRenderer = async ( + _instance: Handsontable.Core, + TD: HTMLTableCellElement, + _row: number, + _col: number, + _prop: string | number, + value: any, + cellProperties: Handsontable.CellProperties, + ) => { + const floatSetting = cellProperties.floatSetting; + TD.style.padding = '0'; + let fields = await metaForm.loadFields(); + while (TD.firstChild) { + TD.removeChild(TD.firstChild); + } + // 如果value不是一个Handsontable实例,则创建一个新的 + if (!cellProperties.subTableInstance) { + let subData: any[] = []; + if (props.readonly) { + subData = floatSetting.subData; + } else { + if (floatSetting.subData.length > 10) { + subData = floatSetting.subData; + } else { + for (var i = 0; i < 10; i++) { + let rowData: any[] = []; + floatSetting.rowsInfo.forEach((info: XRowsInfo) => { + if (info.isLineNumber) { + rowData.push(i + 1); + } else { + rowData.push(undefined); + } + }); + subData.push(rowData); + } + } + } + let subContainer = document.createElement('div'); + subContainer.style.width = '100%'; + subContainer.style.height = '231px'; + TD.appendChild(subContainer); + value = new Handsontable(subContainer, { + data: subData, + width: '100%', + stretchH: 'all', + colWidths: floatSetting.colWidths, + licenseKey: 'non-commercial-and-evaluation', + }); + const updataValueData = () => { + const result = updataCells( + floatSetting, + [], + attrMap, + fields, + props.readonly, + reportStatus, + ); + const cells: CellSettings[] = result.cells; + value.updateSettings({ + cell: cells, + }); + }; + const changes = await refreshFormData( + props.formData, + attrMap, + floatSetting.cells, + floatSetting.coords, + ); + value.batch(async () => { + if (!props.readonly) { + value.updateSettings({ + contextMenu: { + items: { + custom_item: { + name: '增加行', + callback: function (_key: any, _options: any) { + insertRows(); + }, + }, + }, + }, + }); + } + for (const change of changes) { + value.setDataAtCell(...change); + } + updataValueData(); + }); + const insertRows = async () => { + let tempCells = [...floatSetting.cells]; + const subDataLength = subData.length; + const results = await generateProperties( + metaForm, + floatSetting, + tempCells, + subDataLength, + ); + if (results) { + results.arrs?.forEach((arr) => { + metaForm.metadata.attributes.push(arr); + attrMap[arr.id] = arr; + }); + fields = await metaForm.loadFields(true); + tempCells = results.cells; + subData = [...subData, ...results.subData]; + } + floatSetting.cells = tempCells; + floatSetting.subData = subData; + const sheetListData: any = JSON.parse(metaForm.metadata?.reportDatas); + const selectItem = Object.values(sheetListData)[0] as ReportInfo; + const setting = selectItem?.data?.setting || {}; + setting.floatRowsSetting.forEach((settingInfo: XFloatRowsInfo) => { + if (settingInfo.coords == floatSetting.coords) { + settingInfo.subData = subData; + settingInfo.cells = tempCells; + } + }); + metaForm.metadata.reportDatas = JSON.stringify(sheetListData); + await metaForm.save(); + updataValueData(); + message.success('插入成功'); + value.updateSettings({ + data: subData, + }); + }; + value.addHook( + 'afterChange', + function (changes: CellChange[] | null, source: ChangeSource) { + if ( + (source === 'edit' || + source === 'CopyPaste.paste' || + source === 'Autofill.fill') && + !props.readonly + ) { + changes?.forEach((change: CellChange) => { + var row = change[0]; + var col = change[1]; + var newValue = change[3]; + for (var i = 0; i < floatSetting.cells.length; i++) { + const cell = floatSetting.cells[i]; + if (cell.row == row && cell.col == col) { + const prop = attrMap[cell.prop.id]; + + let canChange = false; + if (isDirectEditable(prop) || source === 'edit') { + if (prop.widget == '数字框' && typeof newValue === 'string') { + newValue = parseFloat(newValue); + if (!isNaN(newValue)) { + canChange = true; + } + } else { + canChange = true; + } + } + console.log(prop.propId, canChange, newValue, 'klala23999'); + + if (canChange) { + onValueChange(prop.propId, newValue); + } else { + // 阻止粘贴和自动填充分类型与用户型,还原值 + const key = `${row},${col}`; + value.setDataAtCell( + row, + col as number, + props.formData?.after[0]?.[key], + ); + } + return; + } + } + }); + } + }, + ); + value.addHook( + 'afterOnCellMouseDown', + function (_event: MouseEvent, coords: Handsontable.CellCoords) { + if (!props.readonly) { + floatSetting.cells.forEach(async (item: CellInfo) => { + if (item.col === coords.col && item.row === coords.row) { + const prop = attrMap[item.prop.id]; + if (isSelectWidget(getWidget(prop.valueType, prop.widget))) { + setCoordinate({ + col: floatSetting.mergeCells.yMin, + row: floatSetting.mergeCells.xMin, + }); + setFloatCoord({ col: item.col, row: item.row }); + setEditMode(true); + setSelectValue(undefined); + stagData.forEach((items: stagDataInfo) => { + if (items.col === item.col && items.row === item.row) { + setSelectValue(items.value); + } + }); + fields.map((it) => { + if (it.id == prop.id) { + setField(it); + } + }); + } + } + }); + } + }, + ); + cellProperties.subTable = { + instance: value, + }; + } + }; + const afterOnCellMouseDown = (_event: MouseEvent, coords: Handsontable.CellCoords) => { if (!props.readonly) { cells.forEach((item: CellInfo) => { const prop = attrMap[item.prop.id]; if (item.col === coords.col && item.row === coords.row) { - switch (getWidget(prop.valueType, prop.widget)) { - case '选择框': - case '单选框': - case '引用选择框': - case '多级选择框': - case '人员搜索框': - case '单位搜索框': - case '群组搜索框': - case '组织群搜索框': - case '成员选择框': - case '内部机构选择框': - case '日期选择框': - case '时间选择框': - setCoordinate({ col: item.col, row: item.row }); - setEditMode(true); - setSelectValue(undefined); - stagData.forEach((items: stagDataInfo) => { - if (items.col === item.col && items.row === item.row) { - setSelectValue(items.value); - } - }); - props.fields.map((it) => { - if (it.id == prop.id) { - setField(it); - } - }); - break; + if (isSelectWidget(getWidget(prop.valueType, prop.widget))) { + setCoordinate({ col: item.col, row: item.row }); + setEditMode(true); + setSelectValue(undefined); + stagData.forEach((items: stagDataInfo) => { + if (items.col === item.col && items.row === item.row) { + setSelectValue(items.value); + } + }); + props.fields.map((it) => { + if (it.id == prop.id) { + setField(it); + } + }); } } }); } }; - function afterSelection(row: number, col: number, row2: number, col2: number) { + function afterSelection(row: number, col: number, _row2: number, _col2: number) { const cell = cells.find((c) => c.col == col && c.row == row); if (cell) { setCurrentCell(cell); @@ -498,12 +564,6 @@ const WorkReportViewer: React.FC<{ let canChange = false; if (isDirectEditable(prop) || source === 'edit') { - // 错误把数字格式化成了字符串,而且FormService已经处理过了 - // if (prop.options?.accuracy && prop.widget === '数字框') { - // if (newValue != null) { - // newValue = Number(newValue).toFixed(Number(prop.options?.accuracy)); - // } - // } if (prop.widget == '数字框' && typeof newValue === 'string') { newValue = parseFloat(newValue); if (!isNaN(newValue)) { @@ -614,9 +674,7 @@ const WorkReportViewer: React.FC<{ EditModal.showFormSelect({ form: props.form, fields: props.fields, - belong: props.info?.selectBelong - ? props.service.target.space - : props.service.target, + belong: props.belong, multiple: false, onSave: (values) => { if (values.length > 0) { diff --git a/src/components/DataStandard/ReportForm/config/floatRows.tsx b/src/components/DataStandard/ReportForm/config/floatRows.tsx new file mode 100644 index 000000000..ca838e9c9 --- /dev/null +++ b/src/components/DataStandard/ReportForm/config/floatRows.tsx @@ -0,0 +1,172 @@ +import { Emitter } from '@/ts/base/common'; +import { IForm, IProperty } from '@/ts/core'; +import { Form } from 'devextreme-react'; +import { GroupItem, SimpleItem } from 'devextreme-react/form'; +import React, { useState, useEffect } from 'react'; +import { ValueChangedEvent } from 'devextreme/ui/text_box'; +import useObjectUpdate from '@/hooks/useObjectUpdate'; +import { Button } from 'antd'; +import OpenFileDialog from '@/components/OpenFileDialog'; +import { XFloatRowsInfo } from '../types'; +import cls from '../design/index.module.less'; + +interface IRowInfoProps { + current: IForm; + index: number; + rowInfo: XFloatRowsInfo | undefined; + notifyEmitter: Emitter; +} + +const FloatRowsConfig: React.FC = ({ + current, + rowInfo, + notifyEmitter, +}) => { + const [key, forceUpdate] = useObjectUpdate(current); + const [formData, setFormData] = useState(rowInfo as XFloatRowsInfo); + const [center, setCenter] = useState(<>); + useEffect(() => { + setFormData(rowInfo as XFloatRowsInfo); + }, [rowInfo]); + + const notityAttrChanged = () => { + setFormData({ ...formData }); + }; + + const renderFormItems = () => { + if (!formData?.rowsInfo) { + return <>; + } + const updateRow = (e: ValueChangedEvent, index: number) => { + setFormData((prevFormData) => ({ + ...prevFormData, + rowsInfo: prevFormData.rowsInfo.map((row, i) => { + if (i === index) { + return { ...row, type: e.value }; + } + return row; + }), + })); + forceUpdate(); + }; + return formData?.rowsInfo.map((rowInfo, index) => { + return ( + + + 字段:{rowInfo?.name} + + updateRow(e, index), + }} + /> + {rowInfo?.type === '数字框' && ( + + )} + {rowInfo?.type === '引用型' && ( + + + {rowInfo.applicationData?.name} + + )} + + + + ); + }); + }; + + return ( + <> +
+ + 多选:{formData?.coords} + + { + notifyEmitter.changCallback('isFloatRows', formData); + }, + }} + /> + {formData?.isFloatRows && renderFormItems()} + {rowInfo?.isFloatRows && ( + +
+ +
+
+ )} + + {center} + + ); +}; + +export default FloatRowsConfig; diff --git a/src/components/DataStandard/ReportForm/config/index.tsx b/src/components/DataStandard/ReportForm/config/index.tsx index 0360ba753..5092d2442 100644 --- a/src/components/DataStandard/ReportForm/config/index.tsx +++ b/src/components/DataStandard/ReportForm/config/index.tsx @@ -4,10 +4,13 @@ import { Emitter } from '@/ts/base/common'; import { Tabs } from 'antd'; import AttributeConfig from '../../WorkForm/Design/config/attribute'; import FormConfig from '../../WorkForm/Design/config/form'; +import FloatRowsConfig from './floatRows'; +import { XFloatRowsInfo } from '../types'; interface IAttributeProps { current: IForm; index: number; + rowInfo: XFloatRowsInfo | undefined; notifyEmitter: Emitter; } @@ -35,6 +38,14 @@ const Config: React.FC = (props) => { children: , }); } + if (props.rowInfo) { + items.unshift({ + key: 'rowInfo', + label: '浮动行配置', + forceRender: true, + children: , + }); + } return items; }; return ( diff --git a/src/components/DataStandard/ReportForm/design/components/hotTable.tsx b/src/components/DataStandard/ReportForm/design/components/hotTable.tsx index 332843c91..4ccf6b03c 100644 --- a/src/components/DataStandard/ReportForm/design/components/hotTable.tsx +++ b/src/components/DataStandard/ReportForm/design/components/hotTable.tsx @@ -11,21 +11,16 @@ import OpenFileDialog from '@/components/OpenFileDialog'; import { Emitter } from '@/ts/base/common'; import { HyperFormula } from 'hyperformula'; import { - generateSequence, - generateArrayByLength, - contrast, - replaceRules, - // parsingFormula, -} from './utils'; -import { numberToLetters } from './../../Utils'; -import { CellInfo, ReportSettingInfo } from '../../types'; + numberToLetters, + getRanges, + isPointInRange, + getSelected, + generateProperties, +} from './../../Utils'; +import { CellInfo, XFloatRowsInfo, XRowsInfo } from '../../types'; import Handsontable from 'handsontable'; -import { message, Modal } from 'antd'; +import { message } from 'antd'; import { XAttribute } from '@/ts/base/schema'; -import _ from 'lodash'; -import * as el from '@/utils/excel'; -import { Uploader } from './uploadTemplate'; -import { CellSettings } from 'handsontable/settings'; interface IProps { current: IForm; sheetList: any; @@ -36,6 +31,7 @@ interface IProps { notityEmitter: Emitter; handEcho: (cellStyle: any) => void; selectCellItem: (cell: any) => void; + selectRow: (rowInfo: XFloatRowsInfo | undefined) => void; } const HotTableView: React.FC = ({ @@ -48,6 +44,7 @@ const HotTableView: React.FC = ({ notityEmitter, handEcho, selectCellItem, + selectRow, }) => { const [modalType, setModalType] = useState(''); const [cells, setCells] = useState([]); @@ -56,16 +53,13 @@ const HotTableView: React.FC = ({ const [customBorders, setCustomBorders] = useState([]); const [copySelected, setCopySelected] = useState(); const [selectAttr, setSelectAttr] = useState(); - const [beforeCols, setBeforeCols] = useState([]); - const [beforeRows, setBeforeRows] = useState([]); const [grdatr, setGrdatr] = useState({}); - const initRowCount: number = 30; - const initColCount: number = 4; - const defaultRowHeight: number = 26; + const [floatRowsInfos, setFloatRowsInfos] = useState([]); + const initRowCount: number = 60; + const initColCount: number = 8; + const defaultRowHeight: number = 23; + const defaultColWidth: number = 50; const hotRef = useRef<{ hotInstance: Handsontable }>(null!); // ref - const reportFormView = useRef(null); - const [importData, setImportData] = useState(); - const [defaultColWidth, setDefaultColWidth] = useState(100); const hyperformulaInstance = HyperFormula.buildEmpty({ licenseKey: 'internal-use-in-handsontable', @@ -85,9 +79,7 @@ const HotTableView: React.FC = ({ const selectItem: any = Object.values(sheetListData)[0]; const setting = selectItem?.data?.setting || {}; const datas = selectItem?.data?.data || [[]]; - if (reportFormView.current) { - setDefaultColWidth(Math.floor(reportFormView.current?.offsetWidth / 4)); - } + setFloatRowsInfos(setting.floatRowsSetting || []); updateHot(setting, datas); }, [current]); @@ -96,6 +88,10 @@ const HotTableView: React.FC = ({ const id = notityEmitter.subscribe((_, type, data) => { if (type === 'attr') { updateCells(data, cells); + } else if (type === 'row') { + updateFloatRow(data); + } else if (type === 'isFloatRows') { + whetherFloatRows(data); } }); return () => { @@ -103,74 +99,12 @@ const HotTableView: React.FC = ({ }; }, [cells]); - useEffect(() => { - const cells: CellSettings[] = []; - function getCellSetting(row: number, col: number) { - let cell = cells.find((c) => c.col == col && c.row == row); - if (!cell) { - cell = { col, row }; - cells.push(cell); - } - return cell; - } - if (importData) { - const hot = hotRef.current.hotInstance; - setStyleList(importData?.styleList || []); - setClassList(importData?.classList || []); - hot.updateSettings({ - minCols: importData?.col_w.length, - minRows: importData?.row_h.length, - rowHeights: importData?.row_h, - colWidths: importData?.col_w, - data: importData?.datas, - mergeCells: importData?.mergeCells || [], - }); - importData?.styleList.forEach((item: any) => { - const cell = getCellSetting(item.row, item.col); - cell.renderer = ( - instance: Handsontable.Core, - TD: HTMLTableCellElement, - row: number, - col: number, - prop: string | number, - value: any, - cellProperties: Handsontable.CellProperties, - ) => { - textRenderer(instance, TD, row, col, prop, value, cellProperties); - if (item.styles) { - for (let key in item.styles) { - if (key === 'paddingLeft') { - TD.style[key] = item.styles[key] + 'px'; - } else { - TD.style[key as any] = item.styles[key]; - } - } - } - }; - }); - importData?.classList?.forEach((item: any) => { - const cell = getCellSetting(item.row, item.col); - let arr = []; - for (let k in item.class) { - arr.push(item.class[k]); - } - cell.className = arr.join(' '); - }); - hot.updateSettings({ - cell: cells, - }); - } - }, [importData]); - useEffect(() => { /** 根据工具栏类型进行操作 */ switch (changeType) { case 'onSave': saveClickCallback(); break; - case 'importTemplate': - importTemplate(); - break; case 'copyStyle': copyStyle(); return; @@ -191,7 +125,7 @@ const HotTableView: React.FC = ({ function setCellRender(meta: Handsontable.CellProperties, item: CellInfo) { meta.renderer = 'customStylesRenderer'; const prop = attrMap[item.prop.id]; - if ((prop.rule && JSON.parse(prop.rule).id) || prop.options?.isComputed) { + if ((prop?.rule && JSON.parse(prop.rule).id) || prop?.options?.isComputed) { meta.renderType = 'computed'; } else { meta.renderType = 'input'; @@ -204,7 +138,6 @@ const HotTableView: React.FC = ({ const updateHot = (setting: any, data: any) => { const hot = hotRef.current.hotInstance; const mergeCells = setting?.mergeCells || []; - /** 初始化行高和列宽 */ const row_h = []; for (let i = 0; i < initRowCount; i += 1) { @@ -229,16 +162,9 @@ const HotTableView: React.FC = ({ } } setCells(allCells); - setStyleList(setting?.styleList || []); setClassList(setting?.classList || []); setCustomBorders(setting?.customBorders || []); - setBeforeCols( - generateSequence(setting?.col_w ? setting?.col_w.length : initColCount), - ); - setBeforeRows( - generateArrayByLength(setting?.row_h ? setting?.row_h.length : initRowCount), - ); /** 更新报表 */ hot.updateSettings({ minCols: setting?.col_w ? setting?.col_w.length : initColCount, @@ -264,48 +190,26 @@ const HotTableView: React.FC = ({ for (let k in item.class) { arr.push(item.class[k]); } - hotRef.current.hotInstance.setCellMeta( - item.row, - item.col, - 'className', - arr.join(' '), - ); + hot.setCellMeta(item.row, item.col, 'className', arr.join(' ')); }); /** 渲染单元格颜色 */ hot.batch(() => { setting?.cells?.forEach((item: CellInfo) => { - const meta = hotRef.current.hotInstance.getCellMeta(item.row, item.col); - setCellRender(meta, item); + if (!item.isFloatRow) { + const meta = hot.getCellMeta(item.row, item.col); + setCellRender(meta, item); + } + }); + setting?.floatRowsSetting?.forEach((setting: XFloatRowsInfo) => { + if (setting.isFloatRows) { + const cellsRanges = getRanges(setting.mergeCells); + cellsRanges.forEach((range) => { + hot.getCellMeta(range[0], range[1]).readOnly = true; + hot.getCellMeta(range[0], range[1]).renderer = 'floatRowsRenderer'; + }); + } }); - }); - }; - - /** 导入模板 */ - const importTemplate = () => { - const excel = new el.Excel( - current.directory.target.space, - el.getStandardSheets(current.directory), - ); - const modal = Modal.info({ - icon: <>, - okText: '关闭', - width: 610, - className: 'uploader-model', - title: '导入', - maskClosable: true, - content: ( - { - if (data) { - setImportData(data); - modal.destroy(); - } - }} - /> - ), }); }; @@ -398,7 +302,9 @@ const HotTableView: React.FC = ({ /** 设置边框 */ const setBorder = (border: string, { width = 1, color = '#000000' } = {}) => { const customBordersPlugin = hotRef.current.hotInstance.getPlugin('customBorders'); - const { xMin, xMax, yMin, yMax } = getSelected(); + const { xMin, xMax, yMin, yMax } = getSelected( + hotRef.current.hotInstance.getSelectedLast(), + ); const range: any = []; let customBorder: any = {}; switch (border) { @@ -471,32 +377,28 @@ const HotTableView: React.FC = ({ // }); // } // }; - - /** 格式化所选, 返回从左上到右下的坐标,只返回最后一个 */ - const getSelected = () => { - const selected = hotRef.current.hotInstance.getSelectedLast(); // [startRow, startCol, endRow, endCol] - /** 没有选择区域,返回左上角,并标记 */ - if (!selected) { - return { - xMin: 0, - yMin: 0, - xMax: 0, - yMax: 0, - unselected: true, - }; - } - /** 因为会从不同的方向选择,需要重新排序 */ - const xMin = Math.min(selected[0], selected[2]); - const xMax = Math.max(selected[0], selected[2]); - const yMin = Math.min(selected[1], selected[3]); - const yMax = Math.max(selected[1], selected[3]); - return { - xMin, - xMax, - yMin, - yMax, - }; - }; + // const selected = hotRef.current.hotInstance.getSelectedLast(); // [startRow, startCol, endRow, endCol] + // if (!selected) { + // return { + // xMin: 0, + // yMin: 0, + // xMax: 0, + // yMax: 0, + // unselected: true, + // }; + // } + // /** 因为会从不同的方向选择,需要重新排序 */ + // const xMin = Math.min(selected[0], selected[2]); + // const xMax = Math.max(selected[0], selected[2]); + // const yMin = Math.min(selected[1], selected[3]); + // const yMax = Math.max(selected[1], selected[3]); + // return { + // xMin, + // xMax, + // yMin, + // yMax, + // }; + // }; /** 工具栏按钮点击 */ const buttonClickCallback = () => { @@ -593,6 +495,7 @@ const HotTableView: React.FC = ({ // 缩小保存数据大小 // cells.forEach(c => c.prop = _.pick(c.prop as XAttribute, ['id', 'propId'])); + let json = { data: hot.getData(), setting: { @@ -604,25 +507,27 @@ const HotTableView: React.FC = ({ row_h: row_h, col_w: col_w, grdatr: grdatr, + floatRowsSetting: floatRowsInfos, }, }; + sheetList[0].data = json; const newData = Object.assign({}, sheetList); - - let matchArray: string[] = []; - cells.forEach((cell: any) => { - current.metadata.attributes.forEach((attr) => { - if (cell.prop.id === attr.id) { - matchArray.push(attr.id); - } - }); - }); - current.metadata.attributes = current.metadata.attributes.filter((attr) => { - return matchArray.includes(attr.id); - }); - current.metadata.reportDatas = JSON.stringify(newData); + if (floatRowsInfos.length === 0) { + let matchArray: string[] = []; + cells.forEach((cell) => { + current.metadata.attributes.forEach((attr) => { + if (cell.prop.id === attr.id) { + matchArray.push(attr.id); + } + }); + }); + current.metadata.attributes = current.metadata.attributes.filter((attr) => { + return matchArray.includes(attr.id); + }); + } await current.save(); message.success('保存成功'); }; @@ -645,11 +550,19 @@ const HotTableView: React.FC = ({ (it: any) => it.row === coords.row && it.col === coords.col, ); if (cellItem) { + selectRow(undefined); setSelectAttr(cellItem); selectCellItem(cellItem); } else { setSelectAttr(undefined); } + for (var i = 0; i < floatRowsInfos.length; i++) { + if (isPointInRange(coords, floatRowsInfos[i].mergeCells)) { + selectRow(floatRowsInfos[i]); + selectCellItem(undefined); + return; + } + } handEcho(classJson); } }; @@ -661,7 +574,6 @@ const HotTableView: React.FC = ({ changedDatas.forEach((item: CellInfo) => { if (prop.id === item.prop.id) { item.prop = prop; - Object.keys(prop.options!).map((key) => { switch (key) { case 'readOnly': @@ -718,6 +630,62 @@ const HotTableView: React.FC = ({ setCells(changedDatas); }; + const updateFloatRow = async (floatRowsInfo: XFloatRowsInfo) => { + if (floatRowsInfo.isFloatRows) { + let tempCells: any[] = []; + let subData: any[] = []; + const results = await generateProperties(current, floatRowsInfo, tempCells); + if (results) { + results.arrs?.forEach((arr) => { + current.metadata.attributes.push(arr); + attrMap[arr.id] = arr; + }); + tempCells = [...tempCells, ...results.cells]; + subData = [...subData, ...results.subData]; + } + floatRowsInfo.subData = subData; + floatRowsInfo.cells = tempCells; + message.success(`批量生成成功`); + setFloatRowsInfos([...floatRowsInfos, floatRowsInfo]); + } + }; + + const whetherFloatRows = async (floatRowsInfo: XFloatRowsInfo) => { + const hot = hotRef.current.hotInstance; + const cellsRanges = getRanges(floatRowsInfo.mergeCells); + if (!floatRowsInfo.isFloatRows) { + setFloatRowsInfos( + floatRowsInfos.filter((info) => info.coords !== floatRowsInfo.coords), + ); + current.metadata.attributes = current.metadata.attributes.filter( + (attr) => attr.options?.reportTemporaryCoord === floatRowsInfo.coords, + ); + hot.batch(() => { + cellsRanges.forEach((range) => { + hotRef.current.hotInstance.getCellMeta(range[0], range[1]).readOnly = false; + hotRef.current.hotInstance.getCellMeta(range[0], range[1]).renderer = + 'delStylesRenderer'; + }); + }); + } else { + let infos = floatRowsInfos; + let index = infos.findIndex((info) => info.coords === floatRowsInfo.coords); + if (index != -1) { + infos[index] = floatRowsInfo; + } else { + infos.push(floatRowsInfo); + } + setFloatRowsInfos(infos); + hot.batch(() => { + cellsRanges.forEach((range) => { + hotRef.current.hotInstance.getCellMeta(range[0], range[1]).readOnly = true; + hotRef.current.hotInstance.getCellMeta(range[0], range[1]).renderer = + 'floatRowsRenderer'; + }); + }); + } + }; + /** 删除属性背景色 **/ registerRenderer('delStylesRenderer', (hotInstance: any, TD: any, ...rest) => { textRenderer(hotInstance, TD, ...rest); @@ -740,11 +708,11 @@ const HotTableView: React.FC = ({ } }); - /** 渲染只读背景色 **/ - // registerRenderer('readOnlyStylesRenderer', (hotInstance: any, TD: any, ...rest) => { - // textRenderer(hotInstance, TD, ...rest); - // TD.style.background = '#F0FAF0'; - // }); + /** 渲染浮动行背景色 **/ + registerRenderer('floatRowsRenderer', (hotInstance: any, TD: any, ...rest) => { + textRenderer(hotInstance, TD, ...rest); + TD.style.background = '#50B450'; + }); /** 插入属性 */ const setAttributes = (attribute: IProperty) => { @@ -917,59 +885,58 @@ const HotTableView: React.FC = ({ return newMenu; }; - /** 插入行列后更新数据 */ - const updateData = async (index: number, amount: number, source: any) => { - styleList?.forEach((items: any) => { - if (source === 'ContextMenu.rowAbove' || source === 'ContextMenu.rowBelow') { - if (items.row >= index) { - items.row = Number(items.row) + amount; - } - } else { - if (items.col >= index) { - items.col = Number(items.col) + amount; - } - } - }); - cells?.forEach((items: any) => { - if (source === 'ContextMenu.rowAbove' || source === 'ContextMenu.rowBelow') { - if (items.row >= index) { - items.row = Number(items.row) + amount; - } - } else { - if (items.col >= index) { - items.col = Number(items.col) + amount; + const afterSelectionEnd = ( + row: number, + column: number, + row2: number, + column2: number, + _selectionLayerLevel: number, + ) => { + if (row === row2 && column2 - column >= 1) { + const hot = hotRef.current.hotInstance; + const newRow = row + 1; + let newColumn = column == -1 ? 0 : column; + let arr: XRowsInfo[] = []; + for (let i = 0; i < column2 + 1; i++) { + if (i >= newColumn) { + arr.push({ + name: numberToLetters(i + 1), + index: i + 1, + type: '数字框', + isOnlyRead: false, + }); } } - }); - classList?.forEach((items: any) => { - if (source === 'ContextMenu.rowAbove' || source === 'ContextMenu.rowBelow') { - if (items.row >= index) { - items.row = Number(items.row) + amount; - } - } else { - if (items.col >= index) { - items.col = Number(items.col) + amount; - } + let colWidths: any = []; + for (var colIndex = column; colIndex <= column2; colIndex++) { + colWidths.push(hot.getColWidth(colIndex)); } - }); - let sequence: any = []; - let contrastArray = {}; - if (source === 'ContextMenu.columnLeft' || source === 'ContextMenu.columnRight') { - const countCols = hotRef.current.hotInstance.countCols(); - sequence = generateSequence(countCols); - contrastArray = await contrast(beforeCols, sequence, index, amount); - setBeforeCols(sequence); + let floatRowsInfo: XFloatRowsInfo = { + coords: + numberToLetters(newColumn + 1) + + newRow + + ':' + + numberToLetters(column2 + 1) + + newRow, + floatStartLine: newRow, + isFloatRows: false, + floatRowNumber: 20, + rowsInfo: arr, + startColumn: newColumn, + mergeCells: getSelected(hot.getSelectedLast()), + colWidths: colWidths, + cells: [], + subData: [], + }; + selectRow(floatRowsInfo); + selectCellItem(undefined); } else { - const countRows = hotRef.current.hotInstance.countRows(); - sequence = generateArrayByLength(countRows); - contrastArray = await contrast(beforeRows, sequence, index, amount); - setBeforeRows(sequence); + console.log('选中了多行或者没有选中任何行'); } - replaceRules(contrastArray, current.metadata.rule); }; return ( -
+
= ({ multiColumnSorting={true} filters={true} manualRowMove={true} - afterCreateRow={updateData} - afterCreateCol={updateData} contextMenu={{ items: getMenu(), }} outsideClickDeselects={false} licenseKey="non-commercial-and-evaluation" // for non-commercial use only afterOnCellMouseDown={afterOnCellMouseDown} //鼠标点击单元格边角后被调用 + // beforeOnCellMouseDown={(_event, coords, _TD) => { + // if (coords.col == -1) { + // getFloatingRowsInfo(coords.row + 1); + // } + // }} + afterSelectionEnd={afterSelectionEnd} /> {modalType.includes('新增属性') && ( diff --git a/src/components/DataStandard/ReportForm/design/index.tsx b/src/components/DataStandard/ReportForm/design/index.tsx index 51fd7e272..25051a252 100644 --- a/src/components/DataStandard/ReportForm/design/index.tsx +++ b/src/components/DataStandard/ReportForm/design/index.tsx @@ -4,13 +4,20 @@ import ToolBar from './components/tool'; import cls from './index.module.less'; import { IForm } from '@/ts/core'; import { Emitter } from '@/ts/base/common'; +import { XFloatRowsInfo } from '../types'; interface IProps { current: IForm; notityEmitter: Emitter; selectCellItem: (cell: any) => void; + selectRow: (rowInfo: XFloatRowsInfo | undefined) => void; } -const ReportDesign: React.FC = ({ current, notityEmitter, selectCellItem }) => { +const ReportDesign: React.FC = ({ + current, + notityEmitter, + selectCellItem, + selectRow, +}) => { const [reportChange, setReportChange] = useState(); const [changeType, setChangeType] = useState(''); const [classType, setClassType] = useState(''); @@ -51,6 +58,9 @@ const ReportDesign: React.FC = ({ current, notityEmitter, selectCellItem selectCellItem={(cell: any) => { selectCellItem(cell); }} + selectRow={(rowInfo: XFloatRowsInfo | undefined) => { + selectRow(rowInfo); + }} sheetList={sheetList} reportChange={reportChange} changeType={changeType} diff --git a/src/components/DataStandard/ReportForm/index.tsx b/src/components/DataStandard/ReportForm/index.tsx index abecbaf04..03b4f6724 100644 --- a/src/components/DataStandard/ReportForm/index.tsx +++ b/src/components/DataStandard/ReportForm/index.tsx @@ -6,6 +6,7 @@ import { Layout } from 'antd'; import ReportRender from './report'; import { Emitter } from '@/ts/base/common'; import useCtrlUpdate from '@/hooks/useCtrlUpdate'; +import { XFloatRowsInfo } from './types'; import './register'; @@ -17,6 +18,7 @@ interface IFormDesignProps { const WorkFormDesign: React.FC = ({ current }) => { const [key] = useCtrlUpdate(current); const [selectIndex, setSelectIndex] = React.useState(-1); + const [selectRow, setSelectRow] = React.useState(); const [mainWidth, setMainWidth] = React.useState(400); const [notifyEmitter] = useState(new Emitter()); return ( @@ -27,6 +29,7 @@ const WorkFormDesign: React.FC = ({ current }) => { ), @@ -45,10 +48,11 @@ const WorkFormDesign: React.FC = ({ current }) => { ), - [current, selectIndex], + [current, selectIndex, selectRow], )} diff --git a/src/components/DataStandard/ReportForm/report/index.tsx b/src/components/DataStandard/ReportForm/report/index.tsx index a1138d1d5..5f2ab4e18 100644 --- a/src/components/DataStandard/ReportForm/report/index.tsx +++ b/src/components/DataStandard/ReportForm/report/index.tsx @@ -3,12 +3,14 @@ import React from 'react'; import Toolbar, { Item } from 'devextreme-react/toolbar'; import { Emitter } from '@/ts/base/common'; import ReportDesign from '../design/index'; +import { XFloatRowsInfo } from '../types'; const ReportRender: React.FC<{ current: IForm; notityEmitter: Emitter; onItemSelected: (index: number) => void; -}> = ({ current, notityEmitter, onItemSelected }) => { + onRowSelected: (rowInfo: XFloatRowsInfo | undefined) => void; +}> = ({ current, notityEmitter, onItemSelected, onRowSelected }) => { if (current.metadata.attributes === undefined) { current.metadata.attributes = []; } @@ -39,6 +41,9 @@ const ReportRender: React.FC<{ return; } } + }} + selectRow={(rowInfo: XFloatRowsInfo | undefined) => { + onRowSelected(rowInfo); }}>
); diff --git a/src/components/DataStandard/ReportForm/types.ts b/src/components/DataStandard/ReportForm/types.ts index 6423126d6..81e76ff48 100644 --- a/src/components/DataStandard/ReportForm/types.ts +++ b/src/components/DataStandard/ReportForm/types.ts @@ -4,6 +4,7 @@ export interface CellInfo { col: number; row: number; prop: Pick; + isFloatRow?: boolean; } export interface ReportInfo { name: string; @@ -24,6 +25,7 @@ export interface ReportSettingInfo { grdatr?: any; mergeCells?: any[]; datas?: any[]; + floatRowsSetting: []; } interface cell { @@ -46,3 +48,45 @@ declare module 'handsontable/settings' { renderType?: 'text' | 'input' | 'computed'; } } + +export type XFloatRowsInfo = { + // 坐标 + coords: string; + // 是否浮动行 + isFloatRows: boolean; + // 浮动行起始行 + floatStartLine: number; + // 浮动行数 + floatRowNumber: number; + // 列字段 + rowsInfo: XRowsInfo[]; + // 起始列 + startColumn: number; + // 需要合并的单元格 + mergeCells: any; + // 子表的单元格宽度集合 + colWidths: any; + // 子表的单元格属性 + cells: CellInfo[]; + // 子表的数据 + subData: any[][]; +}; + +export type XRowsInfo = { + // 字段名 + name: string; + // 单元格列数 + index: number; + // 单元格类型 + type: string; + // 单元格引用数据 + applicationData?: any; + // 精度 + accuracy?: number; + // 是否只读 + isOnlyRead: boolean; + // 行次 + isLineNumber?: boolean; + // 起始行次 + startLineNumber?: number; +}; -- Gitee From 5f897f69dd3ed4480313a4c6db750f9ee196aab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=BD=B3=E9=91=AB?= <695997094@qq.com> Date: Mon, 26 Aug 2024 14:18:07 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E6=8A=A5=E8=A1=A8=E6=B5=AE=E5=8A=A8?= =?UTF-8?q?=E8=A1=8C=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DataStandard/ReportForm/Utils/index.tsx | 12 +- .../DataStandard/ReportForm/Viewer/index.tsx | 51 ++++--- .../DataStandard/ReportForm/config/index.tsx | 5 +- .../ReportForm/design/components/hotTable.tsx | 124 ++++++++++++++++-- .../DataStandard/ReportForm/report/index.tsx | 2 +- 5 files changed, 163 insertions(+), 31 deletions(-) diff --git a/src/components/DataStandard/ReportForm/Utils/index.tsx b/src/components/DataStandard/ReportForm/Utils/index.tsx index 3f580d8b0..7611c0d3f 100644 --- a/src/components/DataStandard/ReportForm/Utils/index.tsx +++ b/src/components/DataStandard/ReportForm/Utils/index.tsx @@ -278,6 +278,10 @@ export const updataCells = ( }; }; +function isAllDigits(str: string) { + return /^\d+$/.test(str); +} + export const refreshFormData = async ( formData: model.FormEditData | undefined, attrMap: Dictionary, @@ -312,9 +316,11 @@ export const refreshFormData = async ( case '操作人': case '操作组织': if (value) { - let result = await kernel.queryEntityById({ id: value }); - if (result.success && result.data) { - value = result.data.name; + if (isAllDigits(value)) { + let result = await kernel.queryEntityById({ id: value }); + if (result.success && result.data) { + value = result.data.name; + } } } break; diff --git a/src/components/DataStandard/ReportForm/Viewer/index.tsx b/src/components/DataStandard/ReportForm/Viewer/index.tsx index dc4bfb3d3..70bc9a5f2 100644 --- a/src/components/DataStandard/ReportForm/Viewer/index.tsx +++ b/src/components/DataStandard/ReportForm/Viewer/index.tsx @@ -29,7 +29,7 @@ import CellItem from './cellItem'; import WorkFormService from '@/ts/scripting/core/services/WorkFormService'; import Toolbar, { Item } from 'devextreme-react/toolbar'; import { EditModal } from '@/executor/tools/editModal'; -import { XAttribute } from '@/ts/base/schema'; +import { XAttribute, XThing } from '@/ts/base/schema'; import { CellChange, ChangeSource } from 'handsontable/common'; import { Form } from '@/ts/core/thing/standard/form'; import { @@ -64,7 +64,7 @@ const WorkReportViewer: React.FC<{ onValuesChange?: (fieldId: string, value: any, data: any, coord?: any) => void; activeTabKey?: string; height?: string; - service?: WorkFormService; + service: WorkFormService; }> = (props) => { props.data.name = props.form.name; const [editMode, setEditMode] = useState(false); @@ -152,25 +152,32 @@ const WorkReportViewer: React.FC<{ const hot = hotRef.current.hotInstance; let changeArray: [number, number, any][] = []; for (const item of changeEvents) { - props.primary[item.destId] = item.value; - } - for (const item of changeEvents) { - let prop = cells.find((value: any) => { + let change: [number, number, any] | null = null; + let needText = false; + + let prop = cells.find((value) => { return value.prop.id === item.destId; }); - if (prop && (item.value || item.value == 0)) { + if (!prop) { + continue; + } + + if (item.value || item.value == 0) { const field = fieldMap[prop.prop.id]; switch (field?.valueType) { case '选择型': case '分类型': { const value = field.lookups?.find((lookup) => lookup.value === item.value); - changeArray.push([prop.row, prop.col, value?.text]); + change = [prop.row, prop.col, value?.text]; + needText = true; break; } case '用户型': switch (field.widget) { case '操作人': case '操作组织': + // 自动生成的,不赋值 + continue; case '人员搜索框': case '单位搜索框': case '群组搜索框': @@ -178,18 +185,33 @@ const WorkReportViewer: React.FC<{ case '成员选择框': case '内部机构选择框': { const result = await kernel.queryEntityById({ id: item.value }); - changeArray.push([prop.row, prop.col, result.data?.name]); + change = [prop.row, prop.col, result.data?.name]; + needText = true; break; } } break; default: - changeArray.push([prop.row, prop.col, item.value]); + change = [prop.row, prop.col, item.value]; break; } } + + // 给表单赋值 + props.data[item.destId] = item.value; + // 居然没有地方通知更新,依赖Bug运行??? + onValueChange(item.destId, item.value); + + if (change) { + changeArray.push(change); + if (needText) { + const coordinateId = [prop.row, prop.col].join(','); + props.data[coordinateId] = change[2]; + } + } } - hot.setDataAtCell(changeArray); + // 更新handsontable并阻止afterChange事件 + hot.setDataAtCell(changeArray, 'writeData'); }); useEffect(() => { @@ -293,7 +315,6 @@ const WorkReportViewer: React.FC<{ }); }); - // console.log(floatRowsSetting, 'floatRowsSettingfloatRowsSetting'); setTimeout(() => { setReady(true); }, 20); @@ -453,7 +474,6 @@ const WorkReportViewer: React.FC<{ canChange = true; } } - console.log(prop.propId, canChange, newValue, 'klala23999'); if (canChange) { onValueChange(prop.propId, newValue); @@ -607,7 +627,6 @@ const WorkReportViewer: React.FC<{ let res: model.ReportSummaryTreeNodeView; try { res = await reception!.propertySummaryTree(attr, props.form); - console.log(res); } catch (error) { console.error(error); message.error(error instanceof Error ? error.message : String(error)); @@ -674,7 +693,9 @@ const WorkReportViewer: React.FC<{ EditModal.showFormSelect({ form: props.form, fields: props.fields, - belong: props.belong, + belong: props.info?.selectBelong + ? props.service.target.space + : props.service.target, multiple: false, onSave: (values) => { if (values.length > 0) { diff --git a/src/components/DataStandard/ReportForm/config/index.tsx b/src/components/DataStandard/ReportForm/config/index.tsx index 5092d2442..fbf4a0082 100644 --- a/src/components/DataStandard/ReportForm/config/index.tsx +++ b/src/components/DataStandard/ReportForm/config/index.tsx @@ -20,7 +20,10 @@ const Config: React.FC = (props) => { if (props.index > -1) { setActiveTabKey('property'); } - }, [props.index]); + if (props.rowInfo) { + setActiveTabKey('rowInfo'); + } + }, [props]); const loadItems = () => { const items = [ { diff --git a/src/components/DataStandard/ReportForm/design/components/hotTable.tsx b/src/components/DataStandard/ReportForm/design/components/hotTable.tsx index 4ccf6b03c..44df1e94a 100644 --- a/src/components/DataStandard/ReportForm/design/components/hotTable.tsx +++ b/src/components/DataStandard/ReportForm/design/components/hotTable.tsx @@ -17,10 +17,14 @@ import { getSelected, generateProperties, } from './../../Utils'; -import { CellInfo, XFloatRowsInfo, XRowsInfo } from '../../types'; +import { CellInfo, ReportSettingInfo, XFloatRowsInfo, XRowsInfo } from '../../types'; import Handsontable from 'handsontable'; -import { message } from 'antd'; +import { message, Modal } from 'antd'; import { XAttribute } from '@/ts/base/schema'; +import _ from 'lodash'; +import * as el from '@/utils/excel'; +import { Uploader } from './uploadTemplate'; +import { CellSettings } from 'handsontable/settings'; interface IProps { current: IForm; sheetList: any; @@ -53,13 +57,17 @@ const HotTableView: React.FC = ({ const [customBorders, setCustomBorders] = useState([]); const [copySelected, setCopySelected] = useState(); const [selectAttr, setSelectAttr] = useState(); + // const [beforeCols, setBeforeCols] = useState([]); + // const [beforeRows, setBeforeRows] = useState([]); const [grdatr, setGrdatr] = useState({}); const [floatRowsInfos, setFloatRowsInfos] = useState([]); - const initRowCount: number = 60; - const initColCount: number = 8; - const defaultRowHeight: number = 23; - const defaultColWidth: number = 50; + const initRowCount: number = 30; + const initColCount: number = 4; + const defaultRowHeight: number = 26; const hotRef = useRef<{ hotInstance: Handsontable }>(null!); // ref + const reportFormView = useRef(null); + const [importData, setImportData] = useState(); + const [defaultColWidth, setDefaultColWidth] = useState(100); const hyperformulaInstance = HyperFormula.buildEmpty({ licenseKey: 'internal-use-in-handsontable', @@ -80,6 +88,9 @@ const HotTableView: React.FC = ({ const setting = selectItem?.data?.setting || {}; const datas = selectItem?.data?.data || [[]]; setFloatRowsInfos(setting.floatRowsSetting || []); + if (reportFormView.current) { + setDefaultColWidth(Math.floor(reportFormView.current?.offsetWidth / 4)); + } updateHot(setting, datas); }, [current]); @@ -99,12 +110,74 @@ const HotTableView: React.FC = ({ }; }, [cells]); + useEffect(() => { + const cells: CellSettings[] = []; + function getCellSetting(row: number, col: number) { + let cell = cells.find((c) => c.col == col && c.row == row); + if (!cell) { + cell = { col, row }; + cells.push(cell); + } + return cell; + } + if (importData) { + const hot = hotRef.current.hotInstance; + setStyleList(importData?.styleList || []); + setClassList(importData?.classList || []); + hot.updateSettings({ + minCols: importData?.col_w.length, + minRows: importData?.row_h.length, + rowHeights: importData?.row_h, + colWidths: importData?.col_w, + data: importData?.datas, + mergeCells: importData?.mergeCells || [], + }); + importData?.styleList.forEach((item: any) => { + const cell = getCellSetting(item.row, item.col); + cell.renderer = ( + instance: Handsontable.Core, + TD: HTMLTableCellElement, + row: number, + col: number, + prop: string | number, + value: any, + cellProperties: Handsontable.CellProperties, + ) => { + textRenderer(instance, TD, row, col, prop, value, cellProperties); + if (item.styles) { + for (let key in item.styles) { + if (key === 'paddingLeft') { + TD.style[key] = item.styles[key] + 'px'; + } else { + TD.style[key as any] = item.styles[key]; + } + } + } + }; + }); + importData?.classList?.forEach((item: any) => { + const cell = getCellSetting(item.row, item.col); + let arr = []; + for (let k in item.class) { + arr.push(item.class[k]); + } + cell.className = arr.join(' '); + }); + hot.updateSettings({ + cell: cells, + }); + } + }, [importData]); + useEffect(() => { /** 根据工具栏类型进行操作 */ switch (changeType) { case 'onSave': saveClickCallback(); break; + case 'importTemplate': + importTemplate(); + break; case 'copyStyle': copyStyle(); return; @@ -165,6 +238,12 @@ const HotTableView: React.FC = ({ setStyleList(setting?.styleList || []); setClassList(setting?.classList || []); setCustomBorders(setting?.customBorders || []); + // setBeforeCols( + // generateSequence(setting?.col_w ? setting?.col_w.length : initColCount), + // ); + // setBeforeRows( + // generateArrayByLength(setting?.row_h ? setting?.row_h.length : initRowCount), + // ); /** 更新报表 */ hot.updateSettings({ minCols: setting?.col_w ? setting?.col_w.length : initColCount, @@ -213,6 +292,34 @@ const HotTableView: React.FC = ({ }); }; + /** 导入模板 */ + const importTemplate = () => { + const excel = new el.Excel( + current.directory.target.space, + el.getStandardSheets(current.directory), + ); + const modal = Modal.info({ + icon: <>, + okText: '关闭', + width: 610, + className: 'uploader-model', + title: '导入', + maskClosable: true, + content: ( + { + if (data) { + setImportData(data); + modal.destroy(); + } + }} + /> + ), + }); + }; + /** 复制样式 */ const copyStyle = () => { const selected = hotRef.current.hotInstance.getSelected() || []; @@ -961,11 +1068,6 @@ const HotTableView: React.FC = ({ outsideClickDeselects={false} licenseKey="non-commercial-and-evaluation" // for non-commercial use only afterOnCellMouseDown={afterOnCellMouseDown} //鼠标点击单元格边角后被调用 - // beforeOnCellMouseDown={(_event, coords, _TD) => { - // if (coords.col == -1) { - // getFloatingRowsInfo(coords.row + 1); - // } - // }} afterSelectionEnd={afterSelectionEnd} /> diff --git a/src/components/DataStandard/ReportForm/report/index.tsx b/src/components/DataStandard/ReportForm/report/index.tsx index 5f2ab4e18..beab94628 100644 --- a/src/components/DataStandard/ReportForm/report/index.tsx +++ b/src/components/DataStandard/ReportForm/report/index.tsx @@ -32,7 +32,7 @@ const ReportRender: React.FC<{ current={current} notityEmitter={notityEmitter} selectCellItem={(cell: any) => { - if (cell.prop) { + if (cell && cell.prop) { const index = current.metadata.attributes.findIndex( (i) => i.id === cell.prop?.id, ); -- Gitee