|
|
@@ -7,28 +7,40 @@
|
|
|
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
|
|
*/
|
|
|
|
|
|
-
|
|
|
-
|
|
|
import KCIMPagecontainer from '@/components/KCIMPageContainer';
|
|
|
import { KCIMTable } from '@/components/KCIMTable';
|
|
|
|
|
|
import { createFromIconfontCN } from '@ant-design/icons';
|
|
|
|
|
|
-
|
|
|
import { ActionType, ProFormInstance } from '@ant-design/pro-components';
|
|
|
-import { ModalForm } from '@ant-design/pro-form'
|
|
|
+import { ModalForm } from '@ant-design/pro-form';
|
|
|
import { ProColumns } from '@ant-design/pro-table';
|
|
|
-import { Modal, message, Drawer, Tabs, Input, DatePicker, Popover, Alert, Skeleton } from 'antd';
|
|
|
-import { Key, useEffect, useRef, useState } from 'react';
|
|
|
+import {
|
|
|
+ Modal,
|
|
|
+ message,
|
|
|
+ Drawer,
|
|
|
+ Tabs,
|
|
|
+ Input,
|
|
|
+ DatePicker,
|
|
|
+ Popover,
|
|
|
+ Alert,
|
|
|
+ Skeleton,
|
|
|
+} from 'antd';
|
|
|
+import { Key, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
import * as XLSX from 'xlsx-js-style';
|
|
|
import { saveAs } from 'file-saver';
|
|
|
import moment from 'moment';
|
|
|
import 'moment/locale/zh-cn';
|
|
|
import locale from 'antd/es/date-picker/locale/zh_CN';
|
|
|
|
|
|
-
|
|
|
-
|
|
|
-import { addData, computeProfitReq, getReportDataReq, getReportProjectSettingList, getResponsibleCenters, saveReportRelation } from './service';
|
|
|
+import {
|
|
|
+ addData,
|
|
|
+ computeProfitReq,
|
|
|
+ getReportDataReq,
|
|
|
+ getReportProjectSettingList,
|
|
|
+ getResponsibleCenters,
|
|
|
+ saveReportRelation,
|
|
|
+} from './service';
|
|
|
|
|
|
import './style.less';
|
|
|
|
|
|
@@ -41,910 +53,1484 @@ import { useModel } from '@umijs/max';
|
|
|
import { getUserHasReports } from '@/pages/costAccounting/calcPageTemplate/service';
|
|
|
|
|
|
const IconFont = createFromIconfontCN({
|
|
|
- scriptUrl: '',
|
|
|
+ scriptUrl: '',
|
|
|
});
|
|
|
|
|
|
-const DEFAULT_FONT_COLOR = 'rgba(0, 0, 0, 0.85)';
|
|
|
+const getRawReportFontColor = (record: any) => {
|
|
|
+ const rawColor =
|
|
|
+ record?.fontColor ||
|
|
|
+ record?.fontcolor ||
|
|
|
+ record?.font_color ||
|
|
|
+ record?.color ||
|
|
|
+ record?.font;
|
|
|
+ if (typeof rawColor !== 'string') return undefined;
|
|
|
+ const color = rawColor.trim().replace(/^['"]|['"]$/g, '');
|
|
|
+ if (!color) return undefined;
|
|
|
+ return color;
|
|
|
+};
|
|
|
|
|
|
-const normalizeColor = (rawColor: any) => {
|
|
|
- if (typeof rawColor !== 'string') return DEFAULT_FONT_COLOR;
|
|
|
- const color = rawColor.trim();
|
|
|
- if (!color) return DEFAULT_FONT_COLOR;
|
|
|
- return color;
|
|
|
+const toHex2 = (num: number) =>
|
|
|
+ Math.max(0, Math.min(255, Math.round(num)))
|
|
|
+ .toString(16)
|
|
|
+ .padStart(2, '0')
|
|
|
+ .toUpperCase();
|
|
|
+
|
|
|
+const cssColorCache = new Map<string, string | undefined>();
|
|
|
+
|
|
|
+const convertToExcelRgb = (rawColor: any) => {
|
|
|
+ if (typeof rawColor !== 'string') return undefined;
|
|
|
+ const color = rawColor.trim().replace(/^['"]|['"]$/g, '');
|
|
|
+ if (!color) return undefined;
|
|
|
+ if (cssColorCache.has(color)) return cssColorCache.get(color);
|
|
|
+ const hexColorMatch = color.match(
|
|
|
+ /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/,
|
|
|
+ );
|
|
|
+ if (hexColorMatch) {
|
|
|
+ const hex = hexColorMatch[1];
|
|
|
+ if (hex.length === 3) {
|
|
|
+ const r = hex[0] + hex[0];
|
|
|
+ const g = hex[1] + hex[1];
|
|
|
+ const b = hex[2] + hex[2];
|
|
|
+ const result = (r + g + b).toUpperCase();
|
|
|
+ cssColorCache.set(color, result);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ if (hex.length === 6) {
|
|
|
+ const result = hex.toUpperCase();
|
|
|
+ cssColorCache.set(color, result);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ // #RRGGBBAA 忽略 alpha,取 RRGGBB,兼容更多表格软件
|
|
|
+ const result = hex.slice(0, 6).toUpperCase();
|
|
|
+ cssColorCache.set(color, result);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ const rgbaMatch = color.match(
|
|
|
+ /^rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)\s*(?:,\s*([0-9.]+)\s*)?\)$/i,
|
|
|
+ );
|
|
|
+ if (rgbaMatch) {
|
|
|
+ const r = Number(rgbaMatch[1]);
|
|
|
+ const g = Number(rgbaMatch[2]);
|
|
|
+ const b = Number(rgbaMatch[3]);
|
|
|
+ const result = `${toHex2(r)}${toHex2(g)}${toHex2(b)}`;
|
|
|
+ cssColorCache.set(color, result);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 兜底:交给浏览器解析命名色/hsl 等 CSS 颜色格式
|
|
|
+ if (typeof document !== 'undefined') {
|
|
|
+ const temp = document.createElement('span');
|
|
|
+ temp.style.color = '';
|
|
|
+ temp.style.color = color;
|
|
|
+ if (temp.style.color) {
|
|
|
+ temp.style.display = 'none';
|
|
|
+ document.body.appendChild(temp);
|
|
|
+ const computed = window.getComputedStyle(temp).color;
|
|
|
+ document.body.removeChild(temp);
|
|
|
+ const computedMatch = computed.match(
|
|
|
+ /^rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)\s*(?:,\s*([0-9.]+)\s*)?\)$/i,
|
|
|
+ );
|
|
|
+ if (computedMatch) {
|
|
|
+ const r = Number(computedMatch[1]);
|
|
|
+ const g = Number(computedMatch[2]);
|
|
|
+ const b = Number(computedMatch[3]);
|
|
|
+ const result = `${toHex2(r)}${toHex2(g)}${toHex2(b)}`;
|
|
|
+ cssColorCache.set(color, result);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ cssColorCache.set(color, undefined);
|
|
|
+ return undefined;
|
|
|
};
|
|
|
|
|
|
-const getReportFontColor = (record: any) =>
|
|
|
- normalizeColor(record?.fontColor || record?.fontcolor || record?.font_color || DEFAULT_FONT_COLOR);
|
|
|
+const DEFAULT_IGNORED_EXCEL_RGB = new Set(['000000', '17181A']);
|
|
|
+const pickMeaningfulColor = (excelRgb?: string) =>
|
|
|
+ excelRgb && !DEFAULT_IGNORED_EXCEL_RGB.has(excelRgb) ? excelRgb : undefined;
|
|
|
+
|
|
|
+const collectRenderedRowColorMap = () => {
|
|
|
+ const rowColorMap = new Map<string, string>();
|
|
|
+ if (typeof document === 'undefined') return rowColorMap;
|
|
|
+
|
|
|
+ const rows = document.querySelectorAll(
|
|
|
+ '.departmentCostCalcReportTable .cost-ant-table-body .cost-ant-table-row[data-row-key], .departmentCostCalcReportTable .virtual-table-row[data-row-key]',
|
|
|
+ );
|
|
|
+
|
|
|
+ rows.forEach((rowNode) => {
|
|
|
+ const row = rowNode as HTMLElement;
|
|
|
+ const rowKey = row.getAttribute('data-row-key');
|
|
|
+ if (!rowKey) return;
|
|
|
+
|
|
|
+ const rowStyle = window.getComputedStyle(row);
|
|
|
+ const cssVarColor = rowStyle.getPropertyValue('--report-font-color').trim();
|
|
|
+ const firstCell = row.querySelector(
|
|
|
+ 'td, .virtual-table-cell',
|
|
|
+ ) as HTMLElement | null;
|
|
|
+ const renderedColor = firstCell
|
|
|
+ ? window.getComputedStyle(firstCell).color
|
|
|
+ : '';
|
|
|
+ const finalColor = cssVarColor || renderedColor;
|
|
|
+ if (finalColor) {
|
|
|
+ const excelRgb = pickMeaningfulColor(convertToExcelRgb(finalColor));
|
|
|
+ if (excelRgb) {
|
|
|
+ rowColorMap.set(rowKey, excelRgb);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
|
|
|
+ return rowColorMap;
|
|
|
+};
|
|
|
|
|
|
+const buildTitleColumnColorMap = (
|
|
|
+ title: any[],
|
|
|
+ keyName: 'reportName' | 'responsibilityName',
|
|
|
+) => {
|
|
|
+ const map: Record<string, string> = {};
|
|
|
+
|
|
|
+ const getChildren = (node: any) => {
|
|
|
+ if (Array.isArray(node?.childTitle) && node.childTitle.length > 0)
|
|
|
+ return node.childTitle;
|
|
|
+ if (Array.isArray(node?.child) && node.child.length > 0) return node.child;
|
|
|
+ return [];
|
|
|
+ };
|
|
|
+
|
|
|
+ const walk = (nodes: any[], inheritedColor?: string) => {
|
|
|
+ nodes.forEach((node) => {
|
|
|
+ const currentColor = getRawReportFontColor(node) || inheritedColor;
|
|
|
+ const children = getChildren(node);
|
|
|
+ if (children.length > 0) {
|
|
|
+ walk(children, currentColor);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const key =
|
|
|
+ keyName === 'responsibilityName'
|
|
|
+ ? node?.responsibilityCode
|
|
|
+ : node?.reportId;
|
|
|
+ if (key !== undefined && key !== null && currentColor) {
|
|
|
+ map[String(key)] = currentColor;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
|
|
|
-function findAllParents(tree: any[]) {
|
|
|
- let parents: any[] = [];
|
|
|
- // 递归函数来遍历树并找到所有父节点
|
|
|
- function traverse(nodes: any[]) {
|
|
|
- for (const node of nodes) {
|
|
|
- // 检查节点是否有子节点
|
|
|
- if (node.children && node.children.length > 0) {
|
|
|
- parents.push(node); // 添加到父节点列表
|
|
|
- traverse(node.children); // 递归遍历子节点
|
|
|
- }
|
|
|
- }
|
|
|
+ walk(title || []);
|
|
|
+ return map;
|
|
|
+};
|
|
|
+
|
|
|
+const yieldToBrowser = () =>
|
|
|
+ new Promise<void>((resolve) => {
|
|
|
+ if (
|
|
|
+ typeof window === 'undefined' ||
|
|
|
+ typeof window.requestAnimationFrame !== 'function'
|
|
|
+ ) {
|
|
|
+ setTimeout(resolve, 0);
|
|
|
+ return;
|
|
|
}
|
|
|
+ window.requestAnimationFrame(() => resolve());
|
|
|
+ });
|
|
|
+function findAllParentKeys(tree: any[]) {
|
|
|
+ const parentKeys: Key[] = [];
|
|
|
+
|
|
|
+ function traverse(nodes: any[]) {
|
|
|
+ for (const node of nodes) {
|
|
|
+ if (node.children && node.children.length > 0) {
|
|
|
+ parentKeys.push(node.id);
|
|
|
+ traverse(node.children);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- traverse(tree); // 开始遍历树
|
|
|
- return parents; // 返回所有父节点的数组
|
|
|
+ traverse(tree);
|
|
|
+ return parentKeys;
|
|
|
+}
|
|
|
+
|
|
|
+function findFirstLevelParentKeys(tree: any[]) {
|
|
|
+ return tree
|
|
|
+ .filter((node) => node.children && node.children.length > 0)
|
|
|
+ .map((node) => node.id);
|
|
|
}
|
|
|
|
|
|
function countLeafNodes(trees: any[]) {
|
|
|
- let leafCount = 0;
|
|
|
+ let leafCount = 0;
|
|
|
|
|
|
- // 遍历集合中的每棵树
|
|
|
- for (let i = 0; i < trees.length; i++) {
|
|
|
- leafCount += countLeafNodesRecursive(trees[i]);
|
|
|
- }
|
|
|
+ // 遍历集合中的每棵树
|
|
|
+ for (let i = 0; i < trees.length; i++) {
|
|
|
+ leafCount += countLeafNodesRecursive(trees[i]);
|
|
|
+ }
|
|
|
|
|
|
- return leafCount;
|
|
|
+ return leafCount;
|
|
|
}
|
|
|
|
|
|
function countLeafNodesRecursive(node: any) {
|
|
|
- // 如果当前节点没有子节点,说明它是一个叶子节点
|
|
|
- if (!node.children || node.children.length === 0) {
|
|
|
- return 1;
|
|
|
- }
|
|
|
+ // 如果当前节点没有子节点,说明它是一个叶子节点
|
|
|
+ if (!node.children || node.children.length === 0) {
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
|
|
|
- let leafCount = 0;
|
|
|
+ let leafCount = 0;
|
|
|
|
|
|
- // 递归计算每个子节点的叶子节点数
|
|
|
- for (let i = 0; i < node.children.length; i++) {
|
|
|
- leafCount += countLeafNodesRecursive(node.children[i]);
|
|
|
- }
|
|
|
+ // 递归计算每个子节点的叶子节点数
|
|
|
+ for (let i = 0; i < node.children.length; i++) {
|
|
|
+ leafCount += countLeafNodesRecursive(node.children[i]);
|
|
|
+ }
|
|
|
|
|
|
- return leafCount;
|
|
|
+ return leafCount;
|
|
|
}
|
|
|
|
|
|
-
|
|
|
function searchTree(tree: any[], searchTerm: string) {
|
|
|
- // 定义结果数组
|
|
|
- let results = [];
|
|
|
+ // 定义结果数组
|
|
|
+ let results = [];
|
|
|
|
|
|
- // 定义递归函数来搜索匹配的节点
|
|
|
- function searchNode(node: any) {
|
|
|
- // 创建一个变量来标记当前节点或其子节点是否匹配
|
|
|
- let isMatch = false;
|
|
|
+ // 定义递归函数来搜索匹配的节点
|
|
|
+ function searchNode(node: any) {
|
|
|
+ // 创建一个变量来标记当前节点或其子节点是否匹配
|
|
|
+ let isMatch = false;
|
|
|
|
|
|
- // 检查当前节点的 name 或 code 是否包含搜索词
|
|
|
- if (node.reportName.includes(searchTerm)) {
|
|
|
- isMatch = true;
|
|
|
- }
|
|
|
+ // 检查当前节点的 name 或 code 是否包含搜索词
|
|
|
+ if (node.reportName.includes(searchTerm)) {
|
|
|
+ isMatch = true;
|
|
|
+ }
|
|
|
|
|
|
- // 复制当前节点,避免修改原始数据
|
|
|
- let newNode = { ...node, children: [] };
|
|
|
-
|
|
|
- // 如果有子节点,递归搜索每个子节点
|
|
|
- if (node.children) {
|
|
|
- for (let child of node.children) {
|
|
|
- let childMatch = searchNode(child);
|
|
|
- // 如果子节点或其子树匹配,添加到新节点的子节点数组中
|
|
|
- if (childMatch) {
|
|
|
- newNode.children.push(childMatch);
|
|
|
- isMatch = true;
|
|
|
- }
|
|
|
- }
|
|
|
+ // 复制当前节点,避免修改原始数据
|
|
|
+ let newNode = { ...node, children: [] };
|
|
|
+
|
|
|
+ // 如果有子节点,递归搜索每个子节点
|
|
|
+ if (node.children) {
|
|
|
+ for (let child of node.children) {
|
|
|
+ let childMatch = searchNode(child);
|
|
|
+ // 如果子节点或其子树匹配,添加到新节点的子节点数组中
|
|
|
+ if (childMatch) {
|
|
|
+ newNode.children.push(childMatch);
|
|
|
+ isMatch = true;
|
|
|
}
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // 如果当前节点或其任何子节点匹配,返回新节点
|
|
|
- // 如果children为空,则不包含children属性
|
|
|
- if (isMatch) {
|
|
|
- if (newNode.children.length === 0) {
|
|
|
- delete newNode.children;
|
|
|
- }
|
|
|
- return newNode;
|
|
|
- } else {
|
|
|
- return null;
|
|
|
- }
|
|
|
+ // 如果当前节点或其任何子节点匹配,返回新节点
|
|
|
+ // 如果children为空,则不包含children属性
|
|
|
+ if (isMatch) {
|
|
|
+ if (newNode.children.length === 0) {
|
|
|
+ delete newNode.children;
|
|
|
+ }
|
|
|
+ return newNode;
|
|
|
+ } else {
|
|
|
+ return null;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // 遍历树的每个顶级节点
|
|
|
- for (let node of tree) {
|
|
|
- let result = searchNode(node);
|
|
|
- if (result) {
|
|
|
- results.push(result);
|
|
|
- }
|
|
|
+ // 遍历树的每个顶级节点
|
|
|
+ for (let node of tree) {
|
|
|
+ let result = searchNode(node);
|
|
|
+ if (result) {
|
|
|
+ results.push(result);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- return results;
|
|
|
+ return results;
|
|
|
}
|
|
|
|
|
|
function processTree(originalData: any[]) {
|
|
|
- return originalData.map(node => {
|
|
|
- // 深复制当前节点
|
|
|
- const newNode = JSON.parse(JSON.stringify(node));
|
|
|
-
|
|
|
- // 如果当前节点有profitList,处理它
|
|
|
- if (newNode.profitList && Array.isArray(newNode.profitList)) {
|
|
|
- newNode.profitList.forEach((profit: any) => {
|
|
|
- // 添加新的键值对到新节点,保持原始值的精度
|
|
|
- newNode[`${profit.reportId}`] = profit.value;
|
|
|
- });
|
|
|
- }
|
|
|
+ return (originalData || []).map((node) => {
|
|
|
+ const nextNode = {
|
|
|
+ ...node,
|
|
|
+ id:
|
|
|
+ node.id ||
|
|
|
+ node.responsibilityCode ||
|
|
|
+ node.reportId ||
|
|
|
+ Math.random().toString(36).substr(2, 9),
|
|
|
+ };
|
|
|
|
|
|
- // 如果节点有子节点,递归处理子节点
|
|
|
- if (node.child && Array.isArray(node.child)) {
|
|
|
- newNode.children = processTree(node.child);
|
|
|
+ if (Array.isArray(node.profitList)) {
|
|
|
+ node.profitList.forEach((profit: any) => {
|
|
|
+ if (profit?.reportId !== undefined) {
|
|
|
+ nextNode[`${profit.reportId}`] = profit.value;
|
|
|
}
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- return newNode;
|
|
|
- });
|
|
|
+ if (Array.isArray(node.child) && node.child.length > 0) {
|
|
|
+ nextNode.children = processTree(node.child);
|
|
|
+ }
|
|
|
+
|
|
|
+ return nextNode;
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
+const EMPTY_STYLE_OBJ = {};
|
|
|
// 递归函数,用于处理多层级标题
|
|
|
-function generateColumns(item: any, titleIndex = 0, title: string, hideRatioColumns = false) {
|
|
|
- // // 检查是否需要隐藏"占比"列,如果title包含"占比",且hideRatioColumns为true,则返回null
|
|
|
- // if (hideRatioColumns && item[`${title}`].includes('占比')) {
|
|
|
- // return null;
|
|
|
- // }
|
|
|
-
|
|
|
- const column: any = title == 'responsibilityName' ? {
|
|
|
- title: item[`${title}`],
|
|
|
- dataIndex: `${item.responsibilityCode}`,
|
|
|
- key: `${item.responsibilityCode}`,
|
|
|
- align: (item.responsibilityCode.indexOf('amount') != -1) ? 'right' : 'right',
|
|
|
- ellipsis: true,
|
|
|
- renderText(num: number, record: any) {
|
|
|
+function generateColumns(
|
|
|
+ item: any,
|
|
|
+ titleIndex = 0,
|
|
|
+ title: string,
|
|
|
+ hideRatioColumns = false,
|
|
|
+ inheritedFontColor?: string,
|
|
|
+) {
|
|
|
+ // // 检查是否需要隐藏"占比"列,如果title包含"占比",且hideRatioColumns为true,则返回null
|
|
|
+ // if (hideRatioColumns && item[`${title}`].includes('占比')) {
|
|
|
+ // return null;
|
|
|
+ // }
|
|
|
+
|
|
|
+ const currentFontColor = getRawReportFontColor(item) || inheritedFontColor;
|
|
|
+ const columnCellStyle = currentFontColor
|
|
|
+ ? { color: currentFontColor }
|
|
|
+ : undefined;
|
|
|
+ const cellProps = columnCellStyle
|
|
|
+ ? { style: columnCellStyle }
|
|
|
+ : EMPTY_STYLE_OBJ;
|
|
|
+
|
|
|
+ const column: any =
|
|
|
+ title == 'responsibilityName'
|
|
|
+ ? {
|
|
|
+ title: item[`${title}`],
|
|
|
+ dataIndex: `${item.responsibilityCode}`,
|
|
|
+ key: `${item.responsibilityCode}`,
|
|
|
+ fontColor: item?.fontColor || item?.fontcolor || item?.font_color,
|
|
|
+ onCell: () => cellProps,
|
|
|
+ onHeaderCell: () => cellProps,
|
|
|
+ align:
|
|
|
+ item.responsibilityCode.indexOf('amount') != -1 ? 'right' : 'right',
|
|
|
+ ellipsis: true,
|
|
|
+ renderText(num: number, record: any) {
|
|
|
if (item.responsibilityCode.indexOf('amount') != -1) {
|
|
|
- const { permil, decimalPlace, dataType } = record;
|
|
|
- if (typeof num === 'number' && !isNaN(num)) {
|
|
|
- // 根据 dataType 判断渲染类型
|
|
|
- if (dataType === 2) {
|
|
|
- // 百分比类型
|
|
|
- return `${(num * 100).toFixed(decimalPlace || 2)}%`;
|
|
|
- } else {
|
|
|
- // 数值类型 (dataType === 1 或未定义)
|
|
|
- // 根据 permil 字段决定是否使用千位分隔符
|
|
|
- const useGrouping = permil === 1;
|
|
|
-
|
|
|
- // 解析 decimalPlace 字段,如果无效或未定义,则默认为2
|
|
|
- let fractionDigits = parseInt(decimalPlace, 10);
|
|
|
- if (isNaN(fractionDigits) || fractionDigits < 0) {
|
|
|
- fractionDigits = 2; // 默认保留两位小数
|
|
|
- }
|
|
|
-
|
|
|
- return num.toLocaleString('en-US', {
|
|
|
- minimumFractionDigits: fractionDigits,
|
|
|
- maximumFractionDigits: fractionDigits,
|
|
|
- useGrouping: useGrouping
|
|
|
- });
|
|
|
- }
|
|
|
+ const { permil, decimalPlace, dataType } = record;
|
|
|
+ if (typeof num === 'number' && !isNaN(num)) {
|
|
|
+ // 根据 dataType 判断渲染类型
|
|
|
+ if (dataType === 2) {
|
|
|
+ // 百分比类型
|
|
|
+ return `${(num * 100).toFixed(decimalPlace || 2)}%`;
|
|
|
} else {
|
|
|
- // 对于非数字或 null/undefined,返回空
|
|
|
- return '';
|
|
|
+ // 数值类型 (dataType === 1 或未定义)
|
|
|
+ // 根据 permil 字段决定是否使用千位分隔符
|
|
|
+ const useGrouping = permil === 1;
|
|
|
+
|
|
|
+ // 解析 decimalPlace 字段,如果无效或未定义,则默认为2
|
|
|
+ let fractionDigits = parseInt(decimalPlace, 10);
|
|
|
+ if (isNaN(fractionDigits) || fractionDigits < 0) {
|
|
|
+ fractionDigits = 2; // 默认保留两位小数
|
|
|
+ }
|
|
|
+
|
|
|
+ return num.toLocaleString('en-US', {
|
|
|
+ minimumFractionDigits: fractionDigits,
|
|
|
+ maximumFractionDigits: fractionDigits,
|
|
|
+ useGrouping: useGrouping,
|
|
|
+ });
|
|
|
}
|
|
|
+ } else {
|
|
|
+ // 对于非数字或 null/undefined,返回空
|
|
|
+ return '';
|
|
|
+ }
|
|
|
} else {
|
|
|
- return num;
|
|
|
+ return num;
|
|
|
}
|
|
|
- },
|
|
|
- } : {
|
|
|
- title: item[`${title}`],
|
|
|
- ellipsis: true,
|
|
|
- dataIndex: `${item[`reportId`]}`,
|
|
|
- key: `${item[`reportId`]}`,
|
|
|
- align: 'right',
|
|
|
- // 新增:根据 dataType 渲染不同格式
|
|
|
- renderText(num: string) {
|
|
|
+ },
|
|
|
+ }
|
|
|
+ : {
|
|
|
+ title: item[`${title}`],
|
|
|
+ ellipsis: true,
|
|
|
+ dataIndex: `${item[`reportId`]}`,
|
|
|
+ key: `${item[`reportId`]}`,
|
|
|
+ fontColor: item?.fontColor || item?.fontcolor || item?.font_color,
|
|
|
+ onCell: () => cellProps,
|
|
|
+ onHeaderCell: () => cellProps,
|
|
|
+ align: 'right',
|
|
|
+ // 新增:根据 dataType 渲染不同格式
|
|
|
+ renderText(num: string) {
|
|
|
// 使用 parseFloat 来保持数字精度
|
|
|
const value = parseFloat(num);
|
|
|
const { permil, decimalPlace, dataType } = item;
|
|
|
-
|
|
|
+
|
|
|
// 检查 value 是否为有效数字
|
|
|
if (typeof value === 'number' && !isNaN(value)) {
|
|
|
- if (dataType === 2) {
|
|
|
- // 百分比类型,保留 decimalPlace 位小数
|
|
|
- return `${(value * 100).toFixed(decimalPlace || 2)}%`;
|
|
|
- } else {
|
|
|
- // 数值类型 (dataType === 1 或未定义)
|
|
|
- // 根据 permil 字段决定是否使用千位分隔符
|
|
|
- const useGrouping = permil === 1;
|
|
|
- // 解析 decimalPlace 字段,如果无效或未定义,则默认为2
|
|
|
- let fractionDigits = parseInt(decimalPlace, 10);
|
|
|
- if (isNaN(fractionDigits) || fractionDigits < 0) {
|
|
|
- fractionDigits = 2; // 默认保留两位小数
|
|
|
- }
|
|
|
- // 返回格式化后的数字
|
|
|
- return value.toLocaleString('en-US', {
|
|
|
- minimumFractionDigits: fractionDigits,
|
|
|
- maximumFractionDigits: fractionDigits,
|
|
|
- useGrouping: useGrouping
|
|
|
- });
|
|
|
+ if (dataType === 2) {
|
|
|
+ // 百分比类型,保留 decimalPlace 位小数
|
|
|
+ return `${(value * 100).toFixed(decimalPlace || 2)}%`;
|
|
|
+ } else {
|
|
|
+ // 数值类型 (dataType === 1 或未定义)
|
|
|
+ // 根据 permil 字段决定是否使用千位分隔符
|
|
|
+ const useGrouping = permil === 1;
|
|
|
+ // 解析 decimalPlace 字段,如果无效或未定义,则默认为2
|
|
|
+ let fractionDigits = parseInt(decimalPlace, 10);
|
|
|
+ if (isNaN(fractionDigits) || fractionDigits < 0) {
|
|
|
+ fractionDigits = 2; // 默认保留两位小数
|
|
|
}
|
|
|
+ // 返回格式化后的数字
|
|
|
+ return value.toLocaleString('en-US', {
|
|
|
+ minimumFractionDigits: fractionDigits,
|
|
|
+ maximumFractionDigits: fractionDigits,
|
|
|
+ useGrouping: useGrouping,
|
|
|
+ });
|
|
|
+ }
|
|
|
} else {
|
|
|
- // 非数字或 null/undefined 返回空
|
|
|
- return '';
|
|
|
+ // 非数字或 null/undefined 返回空
|
|
|
+ return '';
|
|
|
}
|
|
|
- },
|
|
|
- };
|
|
|
-
|
|
|
- // 递归处理子列
|
|
|
- if (item.childTitle && Array.isArray(item.childTitle) && item.childTitle.length > 0) {
|
|
|
- column.children = item.childTitle
|
|
|
- .map((a: any, aindex: number) => generateColumns(a, titleIndex + 1, title, hideRatioColumns))
|
|
|
- .filter((col: any) => col !== null); // 过滤掉null项
|
|
|
- }
|
|
|
- if (item.child && Array.isArray(item.child) && item.child.length > 0) {
|
|
|
- column.children = item.child
|
|
|
- .map((a: any, aindex: number) => generateColumns(a, titleIndex + 1, title, hideRatioColumns))
|
|
|
- .filter((col: any) => col !== null); // 过滤掉null项
|
|
|
- }
|
|
|
-
|
|
|
- return column;
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ // 强制以渲染层包裹颜色,避免被表格内部默认字体色覆盖
|
|
|
+ column.render = (_: any, record: any) => {
|
|
|
+ const value = record?.[column.dataIndex];
|
|
|
+ const text = column.renderText ? column.renderText(value, record) : value;
|
|
|
+ if (!currentFontColor) return text;
|
|
|
+ return <span style={{ color: currentFontColor }}>{text}</span>;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 递归处理子列
|
|
|
+ if (
|
|
|
+ item.childTitle &&
|
|
|
+ Array.isArray(item.childTitle) &&
|
|
|
+ item.childTitle.length > 0
|
|
|
+ ) {
|
|
|
+ column.children = item.childTitle
|
|
|
+ .map((a: any, aindex: number) =>
|
|
|
+ generateColumns(
|
|
|
+ a,
|
|
|
+ titleIndex + 1,
|
|
|
+ title,
|
|
|
+ hideRatioColumns,
|
|
|
+ currentFontColor,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ .filter((col: any) => col !== null); // 过滤掉null项
|
|
|
+ }
|
|
|
+ if (item.child && Array.isArray(item.child) && item.child.length > 0) {
|
|
|
+ column.children = item.child
|
|
|
+ .map((a: any, aindex: number) =>
|
|
|
+ generateColumns(
|
|
|
+ a,
|
|
|
+ titleIndex + 1,
|
|
|
+ title,
|
|
|
+ hideRatioColumns,
|
|
|
+ currentFontColor,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ .filter((col: any) => col !== null); // 过滤掉null项
|
|
|
+ }
|
|
|
+
|
|
|
+ return column;
|
|
|
}
|
|
|
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
function transformTreeData(tree: any[]) {
|
|
|
- // 遍历整个树
|
|
|
- return tree.map(node => {
|
|
|
- // 解构出data中的字段并添加到当前节点
|
|
|
- if (Array.isArray(node.data)) {
|
|
|
- node.data.forEach((item: any) => {
|
|
|
- // 将data数组中的每个对象转换成当前节点的属性
|
|
|
- if (item.code && item.value !== undefined) {
|
|
|
- node[item.code] = item.value;
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
+ return (tree || []).map((node) => {
|
|
|
+ const nextNode = {
|
|
|
+ ...node,
|
|
|
+ id:
|
|
|
+ node.id ||
|
|
|
+ node.responsibilityCode ||
|
|
|
+ node.reportId ||
|
|
|
+ Math.random().toString(36).substr(2, 9),
|
|
|
+ };
|
|
|
|
|
|
- // 递归处理子节点,如果存在子节点
|
|
|
- if (node.children) {
|
|
|
- node.children = transformTreeData(node.children);
|
|
|
+ if (Array.isArray(node.data)) {
|
|
|
+ node.data.forEach((item: any) => {
|
|
|
+ if (item?.code && item.value !== undefined) {
|
|
|
+ nextNode[item.code] = item.value;
|
|
|
}
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- // 返回处理后的节点
|
|
|
- return node;
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-const getNextUnexpandedKeys = (data: any[], expandedKeys: any[] = []) => {
|
|
|
- let keys: any[] = [];
|
|
|
+ if (Array.isArray(node.children) && node.children.length > 0) {
|
|
|
+ nextNode.children = transformTreeData(node.children);
|
|
|
+ }
|
|
|
|
|
|
- const traverse = (nodes: any) => {
|
|
|
- for (const node of nodes) {
|
|
|
- // 如果当前节点还没有展开,就把它加入 keys
|
|
|
- if (!expandedKeys.includes(node.id)) {
|
|
|
- keys.push(node.id);
|
|
|
- }
|
|
|
+ return nextNode;
|
|
|
+ });
|
|
|
+}
|
|
|
|
|
|
- // 如果当前节点已经展开,继续遍历子节点
|
|
|
- if (node.children && expandedKeys.includes(node.id)) {
|
|
|
- traverse(node.children);
|
|
|
- }
|
|
|
- }
|
|
|
- };
|
|
|
+const getNextUnexpandedKeys = (
|
|
|
+ data: any[],
|
|
|
+ expandedKeys: Iterable<any> = [],
|
|
|
+) => {
|
|
|
+ const expandedKeySet =
|
|
|
+ expandedKeys instanceof Set ? expandedKeys : new Set(expandedKeys);
|
|
|
+ let keys: any[] = [];
|
|
|
+
|
|
|
+ const traverse = (nodes: any) => {
|
|
|
+ for (const node of nodes) {
|
|
|
+ // 如果当前节点还没有展开,就把它加入 keys
|
|
|
+ if (!expandedKeySet.has(node.id)) {
|
|
|
+ keys.push(node.id);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果当前节点已经展开,继续遍历子节点
|
|
|
+ if (node.children && expandedKeySet.has(node.id)) {
|
|
|
+ traverse(node.children);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- traverse(data);
|
|
|
+ traverse(data);
|
|
|
|
|
|
- return keys;
|
|
|
+ return keys;
|
|
|
};
|
|
|
|
|
|
+import { VirtualRow } from '@/components/KCIMTable/VirtualRow';
|
|
|
|
|
|
+const drawerVirtualComponents = {
|
|
|
+ body: {
|
|
|
+ row: VirtualRow,
|
|
|
+ },
|
|
|
+};
|
|
|
|
|
|
export default function DepartmentCostCalc() {
|
|
|
-
|
|
|
- const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>({ reportType: 0 });
|
|
|
- const tableRef = useRef<ActionType>();
|
|
|
- const formRef = useRef<ProFormInstance>();
|
|
|
- const [tabs, set_tabs] = useState<any[]>([]);
|
|
|
-
|
|
|
- const { initialState, setInitialState } = useModel('@@initialState');
|
|
|
- const [computeDate, set_computeDate] = useState<string>(initialState ? initialState.computeDate : '');
|
|
|
- const [responsibleCenters, set_responsibleCenters] = useState<any[]>([]);
|
|
|
- const [currentTabKey, set_currentTabKey] = useState<any | undefined>(undefined);
|
|
|
- const [currentTab, set_currentTab] = useState<any | undefined>(undefined);
|
|
|
- const [currentSelectedRespon, set_currentSelectedRespon] = useState<any | undefined>(undefined);
|
|
|
- const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState('');
|
|
|
- const [allParentsKeys, set_allParentsKeys] = useState<Key[]>([]);
|
|
|
- const [drawerTableVisible, set_drawerTableVisible] = useState(false);
|
|
|
- const [dataSource, set_dataSource] = useState<any[]>([]);
|
|
|
- const [tableColumns, set_tableColumns] = useState<any[]>([]);
|
|
|
- const [calcResultText, set_calcResultText] = useState<undefined | string>(undefined);
|
|
|
- const [loadingtransform, set_loadingtransform] = useState(false);
|
|
|
- const [ifShowPercent, set_ifShowPercent] = useState(true);
|
|
|
-
|
|
|
- const columns: ProColumns[] = [
|
|
|
-
|
|
|
- {
|
|
|
- title: '报表项目名称',
|
|
|
- dataIndex: 'reportName',
|
|
|
- width: '50%',
|
|
|
- ellipsis: true,
|
|
|
- renderText(text, record, index, action) {
|
|
|
- const { description } = record;
|
|
|
- return description ? <Popover content={() => <div dangerouslySetInnerHTML={{ __html: description }} />}><span style={{ cursor: 'pointer' }}>{text}</span><IconFont className="hover-icon" style={{ fontSize: 16, color: '#17181a', paddingLeft: 4, position: 'relative', top: 1 }} type={'iconshuoming'} /></Popover> : text
|
|
|
- },
|
|
|
- },
|
|
|
- {
|
|
|
- title: '金额(元)',
|
|
|
- align: 'right',
|
|
|
- dataIndex: 'amount',
|
|
|
- renderText(num, record) {
|
|
|
- const { calcType, permil, decimalPlace, dataType } = record;
|
|
|
- if (record.children && calcType == '0') {
|
|
|
- return <React.Fragment></React.Fragment>
|
|
|
- } else {
|
|
|
- // 检查 num 是否是有效数字
|
|
|
- if (typeof num === 'number' && !isNaN(num)) {
|
|
|
- // 根据 dataType 判断渲染类型
|
|
|
- if (dataType === 2) {
|
|
|
- // 百分比类型
|
|
|
- return `${(num * 100).toFixed(decimalPlace || 2)}%`;
|
|
|
- } else {
|
|
|
- // 数值类型 (dataType === 1 或未定义)
|
|
|
- // 根据 permil 字段决定是否使用千位分隔符
|
|
|
- const useGrouping = permil === 1;
|
|
|
-
|
|
|
- // 解析 decimalPlace 字段,如果无效或未定义,则默认为2
|
|
|
- let fractionDigits = parseInt(decimalPlace, 10);
|
|
|
- if (isNaN(fractionDigits) || fractionDigits < 0) {
|
|
|
- fractionDigits = 2; // 默认保留两位小数
|
|
|
- }
|
|
|
-
|
|
|
- return num.toLocaleString('en-US', {
|
|
|
- minimumFractionDigits: fractionDigits,
|
|
|
- maximumFractionDigits: fractionDigits,
|
|
|
- useGrouping: useGrouping
|
|
|
- });
|
|
|
- }
|
|
|
- } else {
|
|
|
- // 对于非数字或 null/undefined,返回空
|
|
|
- return '';
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- },
|
|
|
- {
|
|
|
- title: '占比',
|
|
|
- align: 'right',
|
|
|
- hideInTable: !ifShowPercent,
|
|
|
- dataIndex: 'percent',
|
|
|
- renderText(num, record) {
|
|
|
- const { calcType, decimalPlace } = record;
|
|
|
- if (record.children && calcType == '0') {
|
|
|
- return <React.Fragment></React.Fragment>
|
|
|
- } else {
|
|
|
- return num != null ? `${((num * 100).toFixed(decimalPlace))}%` : num
|
|
|
- }
|
|
|
-
|
|
|
- },
|
|
|
+ const [tableDataFilterParams, set_tableDataFilterParams] = useState<
|
|
|
+ any | undefined
|
|
|
+ >({ reportType: 0 });
|
|
|
+ const tableRef = useRef<ActionType>();
|
|
|
+ const formRef = useRef<ProFormInstance>();
|
|
|
+ const [tabs, set_tabs] = useState<any[]>([]);
|
|
|
+
|
|
|
+ const { initialState, setInitialState } = useModel('@@initialState');
|
|
|
+ const [computeDate, set_computeDate] = useState<string>(
|
|
|
+ initialState ? initialState.computeDate : '',
|
|
|
+ );
|
|
|
+ const [responsibleCenters, set_responsibleCenters] = useState<any[]>([]);
|
|
|
+ const [currentTabKey, set_currentTabKey] = useState<any | undefined>(
|
|
|
+ undefined,
|
|
|
+ );
|
|
|
+ const [currentTab, set_currentTab] = useState<any | undefined>(undefined);
|
|
|
+ const [currentSelectedRespon, set_currentSelectedRespon] = useState<
|
|
|
+ any | undefined
|
|
|
+ >(undefined);
|
|
|
+ const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState('');
|
|
|
+ const [allParentsKeys, set_allParentsKeys] = useState<Key[]>([]);
|
|
|
+ const [drawerTableVisible, set_drawerTableVisible] = useState(false);
|
|
|
+ const [mainTableDataSource, set_mainTableDataSource] = useState<any[]>([]);
|
|
|
+ const [drawerDataSource, set_drawerDataSource] = useState<any[]>([]);
|
|
|
+ const [drawerExpandedKeys, set_drawerExpandedKeys] = useState<Key[]>([]);
|
|
|
+ const [tableColumns, set_tableColumns] = useState<any[]>([]);
|
|
|
+ const [calcResultText, set_calcResultText] = useState<undefined | string>(
|
|
|
+ undefined,
|
|
|
+ );
|
|
|
+ const [loadingtransform, set_loadingtransform] = useState(false);
|
|
|
+ const [ifShowPercent, set_ifShowPercent] = useState(true);
|
|
|
+ const [titleColumnColorMap, set_titleColumnColorMap] = useState<
|
|
|
+ Record<string, string>
|
|
|
+ >({});
|
|
|
+
|
|
|
+ const columns = useMemo<ProColumns[]>(
|
|
|
+ () => [
|
|
|
+ {
|
|
|
+ title: '报表项目名称',
|
|
|
+ dataIndex: 'reportName',
|
|
|
+ width: '50%',
|
|
|
+ ellipsis: true,
|
|
|
+ renderText(text, record, index, action) {
|
|
|
+ const { description } = record;
|
|
|
+ return description ? (
|
|
|
+ <Popover
|
|
|
+ content={() => (
|
|
|
+ <div dangerouslySetInnerHTML={{ __html: description }} />
|
|
|
+ )}
|
|
|
+ >
|
|
|
+ <span style={{ cursor: 'pointer' }}>{text}</span>
|
|
|
+ <IconFont
|
|
|
+ className="hover-icon"
|
|
|
+ style={{
|
|
|
+ fontSize: 16,
|
|
|
+ color: '#17181a',
|
|
|
+ paddingLeft: 4,
|
|
|
+ position: 'relative',
|
|
|
+ top: 1,
|
|
|
+ }}
|
|
|
+ type={'iconshuoming'}
|
|
|
+ />
|
|
|
+ </Popover>
|
|
|
+ ) : (
|
|
|
+ text
|
|
|
+ );
|
|
|
},
|
|
|
- ];
|
|
|
-
|
|
|
-
|
|
|
- // 主函数,生成表格列
|
|
|
- const generateTableColumns = (title: any[], titleKeyName: string) => {
|
|
|
- return title.map((item: any, titleIndex: number) => generateColumns(item, titleIndex, titleKeyName, !ifShowPercent));
|
|
|
- };
|
|
|
-
|
|
|
-
|
|
|
- const getTableData = async (params: any) => {
|
|
|
- const { responsibilityCode, filter = undefined } = params;
|
|
|
- if (!responsibilityCode) return []
|
|
|
- const resp = await getReportProjectSettingList({ ...params });
|
|
|
-
|
|
|
- if (resp) {
|
|
|
- if (filter) {
|
|
|
- const filterData = searchTree(resp, filter);
|
|
|
- const allParents = findAllParents(filterData);
|
|
|
-
|
|
|
- set_allParentsKeys([...(allParents.map((a: any) => a.id))]);
|
|
|
- set_dataSource([...filterData]);
|
|
|
- return {
|
|
|
- data: filterData,
|
|
|
- success: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '金额(元)',
|
|
|
+ align: 'right',
|
|
|
+ dataIndex: 'amount',
|
|
|
+ renderText(num, record) {
|
|
|
+ const { calcType, permil, decimalPlace, dataType } = record;
|
|
|
+ if (record.children && calcType == '0') {
|
|
|
+ return <React.Fragment></React.Fragment>;
|
|
|
+ } else {
|
|
|
+ // 检查 num 是否是有效数字
|
|
|
+ if (typeof num === 'number' && !isNaN(num)) {
|
|
|
+ // 根据 dataType 判断渲染类型
|
|
|
+ if (dataType === 2) {
|
|
|
+ // 百分比类型
|
|
|
+ return `${(num * 100).toFixed(decimalPlace || 2)}%`;
|
|
|
+ } else {
|
|
|
+ // 数值类型 (dataType === 1 或未定义)
|
|
|
+ // 根据 permil 字段决定是否使用千位分隔符
|
|
|
+ const useGrouping = permil === 1;
|
|
|
+
|
|
|
+ // 解析 decimalPlace 字段,如果无效或未定义,则默认为2
|
|
|
+ let fractionDigits = parseInt(decimalPlace, 10);
|
|
|
+ if (isNaN(fractionDigits) || fractionDigits < 0) {
|
|
|
+ fractionDigits = 2; // 默认保留两位小数
|
|
|
}
|
|
|
- }
|
|
|
- if (currentTab.value == '1') {
|
|
|
- const allParents = findAllParents(resp);
|
|
|
- set_allParentsKeys([...(allParents.map((a: any) => a.id))]);
|
|
|
-
|
|
|
- }
|
|
|
- set_dataSource([...resp]);
|
|
|
|
|
|
- return {
|
|
|
- data: resp,
|
|
|
- success: true,
|
|
|
+ return num.toLocaleString('en-US', {
|
|
|
+ minimumFractionDigits: fractionDigits,
|
|
|
+ maximumFractionDigits: fractionDigits,
|
|
|
+ useGrouping: useGrouping,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 对于非数字或 null/undefined,返回空
|
|
|
+ return '';
|
|
|
}
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '占比',
|
|
|
+ align: 'right',
|
|
|
+ hideInTable: !ifShowPercent,
|
|
|
+ dataIndex: 'percent',
|
|
|
+ renderText(num, record) {
|
|
|
+ const { calcType, decimalPlace } = record;
|
|
|
+ if (record.children && calcType == '0') {
|
|
|
+ return <React.Fragment></React.Fragment>;
|
|
|
+ } else {
|
|
|
+ return num != null ? `${(num * 100).toFixed(decimalPlace)}%` : num;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ [ifShowPercent],
|
|
|
+ );
|
|
|
+
|
|
|
+ // 主函数,生成表格列
|
|
|
+ const generateTableColumns = useCallback(
|
|
|
+ (title: any[], titleKeyName: string) => {
|
|
|
+ return title.map((item: any, titleIndex: number) =>
|
|
|
+ generateColumns(item, titleIndex, titleKeyName, !ifShowPercent),
|
|
|
+ );
|
|
|
+ },
|
|
|
+ [ifShowPercent],
|
|
|
+ );
|
|
|
+
|
|
|
+ const getTableData = useCallback(
|
|
|
+ async (params: any) => {
|
|
|
+ const { responsibilityCode, filter = undefined } = params;
|
|
|
+ if (!responsibilityCode) return [];
|
|
|
+ const resp = await getReportProjectSettingList({ ...params });
|
|
|
+
|
|
|
+ if (resp) {
|
|
|
+ if (filter) {
|
|
|
+ const filterData = searchTree(resp, filter);
|
|
|
+ set_allParentsKeys(findAllParentKeys(filterData));
|
|
|
+ set_mainTableDataSource([...filterData]);
|
|
|
+ return {
|
|
|
+ data: filterData,
|
|
|
+ success: true,
|
|
|
+ };
|
|
|
}
|
|
|
- return []
|
|
|
- }
|
|
|
-
|
|
|
- const getIfshowPercent = async () => {
|
|
|
- const { systemId } = JSON.parse(localStorage.getItem('currentSelectedTab') as string)
|
|
|
- const resp = await getParamsDataBySysId(systemId, '1851077044079824896');
|
|
|
- if (resp) {
|
|
|
- set_ifShowPercent(resp.value == '1' ? true : false);
|
|
|
+ if (currentTab?.value == '1') {
|
|
|
+ set_allParentsKeys(findFirstLevelParentKeys(resp));
|
|
|
}
|
|
|
+ set_mainTableDataSource([...resp]);
|
|
|
+
|
|
|
+ return {
|
|
|
+ data: resp,
|
|
|
+ success: true,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return [];
|
|
|
+ },
|
|
|
+ [currentTab?.value],
|
|
|
+ );
|
|
|
+
|
|
|
+ const getIfshowPercent = async () => {
|
|
|
+ const { systemId } = JSON.parse(
|
|
|
+ localStorage.getItem('currentSelectedTab') as string,
|
|
|
+ );
|
|
|
+ const resp = await getParamsDataBySysId(systemId, '1851077044079824896');
|
|
|
+ if (resp) {
|
|
|
+ set_ifShowPercent(resp.value == '1' ? true : false);
|
|
|
}
|
|
|
-
|
|
|
-
|
|
|
- const onTabChanged = (key: Key) => {
|
|
|
- set_currentTabKey(key);
|
|
|
- const needItem = tabs.filter((a) => a.key == key);
|
|
|
- if (needItem.length > 0) set_currentTab(needItem[0])
|
|
|
+ };
|
|
|
+
|
|
|
+ const onTabChanged = (key: Key) => {
|
|
|
+ set_currentTabKey(key);
|
|
|
+ const needItem = tabs.filter((a) => a.key == key);
|
|
|
+ if (needItem.length > 0) set_currentTab(needItem[0]);
|
|
|
+ };
|
|
|
+
|
|
|
+ const getTabs = async () => {
|
|
|
+ // const { systemId } = JSON.parse((localStorage.getItem('currentSelectedTab')) as string)
|
|
|
+ // const resp = await getDicDataBySysId(systemId, 'PROFIT_REPORT_TYPE');
|
|
|
+ const resp = await getUserHasReports();
|
|
|
+ if (resp) {
|
|
|
+ const { dataVoList } = resp;
|
|
|
+ const tempArr = dataVoList.map((a: any) => ({
|
|
|
+ label: a.name,
|
|
|
+ key: Number(a.code),
|
|
|
+ value: a.value,
|
|
|
+ }));
|
|
|
+ const arr = tempArr.filter((a: any) => a.value != '2');
|
|
|
+ set_tabs([...arr]);
|
|
|
+ set_currentTabKey(arr[0].key);
|
|
|
+ set_currentTab(arr[0]);
|
|
|
}
|
|
|
+ };
|
|
|
|
|
|
- const getTabs = async () => {
|
|
|
-
|
|
|
- // const { systemId } = JSON.parse((localStorage.getItem('currentSelectedTab')) as string)
|
|
|
- // const resp = await getDicDataBySysId(systemId, 'PROFIT_REPORT_TYPE');
|
|
|
- const resp = await getUserHasReports();
|
|
|
+ const getResponsibleCenterList = async (reportType: string) => {
|
|
|
+ const resp = await getResponsibleCenters(reportType);
|
|
|
+ if (resp) {
|
|
|
+ set_responsibleCenters(resp);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const onLeftChange = (currentSelected: any) => {
|
|
|
+ set_currentSelectedRespon(currentSelected);
|
|
|
+ };
|
|
|
+
|
|
|
+ const tableDataSearchHandle = useCallback(
|
|
|
+ (paramName: string) => {
|
|
|
+ set_tableDataFilterParams((prev: any) => ({
|
|
|
+ ...prev,
|
|
|
+ [`${paramName}`]: tableDataSearchKeywords,
|
|
|
+ }));
|
|
|
+ },
|
|
|
+ [tableDataSearchKeywords],
|
|
|
+ );
|
|
|
+
|
|
|
+ const onekeyComputeProfitHandle = async () => {
|
|
|
+ Modal.confirm({
|
|
|
+ title: '注意',
|
|
|
+ content: '一键计算操作会覆盖当月已计算的数据,是否继续操作?',
|
|
|
+ okText: '确定',
|
|
|
+ cancelText: '取消',
|
|
|
+ onOk: async () => {
|
|
|
+ try {
|
|
|
+ const promises = tabs.map((tab) =>
|
|
|
+ computeProfitReq(computeDate, tab.key),
|
|
|
+ ); // 对每个tab创建一个请求
|
|
|
+ const results = await Promise.all(promises); // 等待所有请求完成
|
|
|
+
|
|
|
+ const allSuccess = results.every((resp) => resp); // 检查所有请求是否都成功
|
|
|
+ if (allSuccess) {
|
|
|
+ message.success('操作成功!');
|
|
|
+ set_calcResultText('一键计算成功!');
|
|
|
+ } else {
|
|
|
+ set_calcResultText('一键计算部分失败!');
|
|
|
+ }
|
|
|
+
|
|
|
+ tableRef.current?.reload(); // 重新加载表格数据
|
|
|
+ } catch (error) {
|
|
|
+ message.error('操作失败,请重试!');
|
|
|
+ set_calcResultText('一键计算失败!');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const computeProfitHandle = async () => {
|
|
|
+ Modal.confirm({
|
|
|
+ title: '注意',
|
|
|
+ content: '计算操作会覆盖当月已计算的数据,是否继续操作?',
|
|
|
+ okText: '确定',
|
|
|
+ cancelText: '取消',
|
|
|
+ onOk: async (...args) => {
|
|
|
+ const resp = await computeProfitReq(computeDate, currentTabKey);
|
|
|
if (resp) {
|
|
|
- const { dataVoList } = resp;
|
|
|
- const tempArr = dataVoList.map((a: any) => ({ label: a.name, key: Number(a.code), value: a.value }));
|
|
|
- const arr = (tempArr.filter((a: any) => a.value != '2'));
|
|
|
- set_tabs([...arr]);
|
|
|
- set_currentTabKey(arr[0].key);
|
|
|
- set_currentTab(arr[0]);
|
|
|
+ message.success('操作成功!');
|
|
|
+ set_calcResultText('计算成功!');
|
|
|
+ tableRef.current?.reload();
|
|
|
+ } else {
|
|
|
+ set_calcResultText('计算失败!');
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- const getResponsibleCenterList = async (reportType: string) => {
|
|
|
- const resp = await getResponsibleCenters(reportType);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const openTableDataDrawer = async () => {
|
|
|
+ set_drawerTableVisible(true);
|
|
|
+ set_loadingtransform(true);
|
|
|
+ try {
|
|
|
+ if (currentTab?.value == '3') {
|
|
|
+ const resp = await getReportDataReq(currentTabKey, computeDate, '3');
|
|
|
if (resp) {
|
|
|
- set_responsibleCenters(resp);
|
|
|
+ await yieldToBrowser();
|
|
|
+ const { title = [], data = [] } = resp;
|
|
|
+ const defaultColumns: ProColumns[] = [
|
|
|
+ {
|
|
|
+ title: '报表项目名称',
|
|
|
+ dataIndex: 'reportName',
|
|
|
+ key: 'reportName',
|
|
|
+ width: 220,
|
|
|
+ fixed: 'left',
|
|
|
+ },
|
|
|
+ ];
|
|
|
+ const nextTableColumns = generateTableColumns(
|
|
|
+ title,
|
|
|
+ 'responsibilityName',
|
|
|
+ );
|
|
|
+ const nextDataSource = transformTreeData(data);
|
|
|
+ set_tableColumns([...defaultColumns, ...nextTableColumns]);
|
|
|
+ set_titleColumnColorMap(
|
|
|
+ buildTitleColumnColorMap(title, 'responsibilityName'),
|
|
|
+ );
|
|
|
+ set_drawerDataSource(nextDataSource);
|
|
|
+ set_drawerExpandedKeys(findFirstLevelParentKeys(nextDataSource));
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- const onLeftChange = (currentSelected: any) => {
|
|
|
- set_currentSelectedRespon(currentSelected);
|
|
|
- }
|
|
|
-
|
|
|
- const tableDataSearchHandle = (paramName: string) => {
|
|
|
-
|
|
|
- set_tableDataFilterParams({
|
|
|
- ...tableDataFilterParams,
|
|
|
- [`${paramName}`]: tableDataSearchKeywords
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- const onekeyComputeProfitHandle = async () => {
|
|
|
- Modal.confirm({
|
|
|
- title: '注意',
|
|
|
- content: '一键计算操作会覆盖当月已计算的数据,是否继续操作?',
|
|
|
- okText: '确定',
|
|
|
- cancelText: '取消',
|
|
|
- onOk: async () => {
|
|
|
- try {
|
|
|
- const promises = tabs.map(tab => computeProfitReq(computeDate, tab.key)); // 对每个tab创建一个请求
|
|
|
- const results = await Promise.all(promises); // 等待所有请求完成
|
|
|
-
|
|
|
- const allSuccess = results.every(resp => resp); // 检查所有请求是否都成功
|
|
|
- if (allSuccess) {
|
|
|
- message.success('操作成功!');
|
|
|
- set_calcResultText('一键计算成功!');
|
|
|
- } else {
|
|
|
- set_calcResultText('一键计算部分失败!');
|
|
|
- }
|
|
|
-
|
|
|
- tableRef.current?.reload(); // 重新加载表格数据
|
|
|
- } catch (error) {
|
|
|
- message.error('操作失败,请重试!');
|
|
|
- set_calcResultText('一键计算失败!');
|
|
|
- }
|
|
|
+ } else if (currentTab?.value) {
|
|
|
+ const resp = await getReportDataReq(
|
|
|
+ currentTabKey,
|
|
|
+ computeDate,
|
|
|
+ currentTab.value,
|
|
|
+ );
|
|
|
+ if (resp) {
|
|
|
+ await yieldToBrowser();
|
|
|
+ const { title = [], data = [] } = resp;
|
|
|
+ const defaultColumns = [
|
|
|
+ {
|
|
|
+ title: '科室名称',
|
|
|
+ dataIndex: 'responsibilityName',
|
|
|
+ key: 'responsibilityName',
|
|
|
+ width: 220,
|
|
|
+ fixed: 'left',
|
|
|
},
|
|
|
+ ];
|
|
|
+
|
|
|
+ const nextTableColumns = generateTableColumns(title, 'reportName');
|
|
|
+ const nextDataSource = processTree(data);
|
|
|
+ set_tableColumns([...defaultColumns, ...nextTableColumns]);
|
|
|
+ set_titleColumnColorMap(
|
|
|
+ buildTitleColumnColorMap(title, 'reportName'),
|
|
|
+ );
|
|
|
+ set_drawerDataSource(nextDataSource);
|
|
|
+ set_drawerExpandedKeys(findFirstLevelParentKeys(nextDataSource));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ set_loadingtransform(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const getHeaderRows = (
|
|
|
+ columns: any[],
|
|
|
+ level = 0,
|
|
|
+ headerRows: any[] = [],
|
|
|
+ maxLevel = 0,
|
|
|
+ ) => {
|
|
|
+ // 规则变更:将叶子列标题统一下沉至最底行(maxLevel - 1),
|
|
|
+ // 上方各层以空白占位,避免纵向合并导致“高单元格”。
|
|
|
+ headerRows[level] = headerRows[level] || [];
|
|
|
+ columns.forEach((col: { title: any; children: any }) => {
|
|
|
+ const colSpan = getColSpan(col);
|
|
|
+ if (col.children) {
|
|
|
+ // 父节点仅进行横向合并,不做纵向合并
|
|
|
+ headerRows[level].push({ title: col.title, colSpan, rowSpan: 1 });
|
|
|
+ getHeaderRows(col.children, level + 1, headerRows, maxLevel);
|
|
|
+ } else {
|
|
|
+ // 叶子:从当前层到倒数第二层使用空白占位,叶子标题写入最底层
|
|
|
+ for (let i = level; i < maxLevel - 1; i++) {
|
|
|
+ headerRows[i] = headerRows[i] || [];
|
|
|
+ headerRows[i].push({ title: '', colSpan: 1, rowSpan: 1 });
|
|
|
+ }
|
|
|
+ headerRows[maxLevel - 1] = headerRows[maxLevel - 1] || [];
|
|
|
+ headerRows[maxLevel - 1].push({
|
|
|
+ title: col.title,
|
|
|
+ colSpan: 1,
|
|
|
+ rowSpan: 1,
|
|
|
});
|
|
|
- };
|
|
|
-
|
|
|
-
|
|
|
- const computeProfitHandle = async () => {
|
|
|
-
|
|
|
- Modal.confirm({
|
|
|
- title: '注意',
|
|
|
- content: '计算操作会覆盖当月已计算的数据,是否继续操作?',
|
|
|
- okText: '确定',
|
|
|
- cancelText: '取消',
|
|
|
- onOk: async (...args) => {
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return headerRows;
|
|
|
+ };
|
|
|
+
|
|
|
+ const getColSpan: any = (col: { children: any[] }) => {
|
|
|
+ if (!col.children) return 1;
|
|
|
+ return col.children.reduce((sum, child) => sum + getColSpan(child), 0);
|
|
|
+ };
|
|
|
+
|
|
|
+ const getMaxLevel = (col: any) => {
|
|
|
+ if (!col.children) return 1;
|
|
|
+ return 1 + Math.max(...col.children.map(getMaxLevel));
|
|
|
+ };
|
|
|
+
|
|
|
+ const extractLeafColumnsWithInheritedColor = (
|
|
|
+ columns: any[],
|
|
|
+ inheritedColor?: string,
|
|
|
+ ) => {
|
|
|
+ let leafColumns: Array<{ column: any; color?: string }> = [];
|
|
|
+ columns.forEach((col) => {
|
|
|
+ const currentColor = getRawReportFontColor(col) || inheritedColor;
|
|
|
+ if (col.children && col.children.length > 0) {
|
|
|
+ leafColumns = leafColumns.concat(
|
|
|
+ extractLeafColumnsWithInheritedColor(col.children, currentColor),
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ leafColumns.push({ column: col, color: currentColor });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return leafColumns;
|
|
|
+ };
|
|
|
+
|
|
|
+ const addRowWithIndentation = (
|
|
|
+ record: any,
|
|
|
+ level: number,
|
|
|
+ leafColumns: any[],
|
|
|
+ worksheetData: any[],
|
|
|
+ dataRowColors: Array<string | undefined>,
|
|
|
+ renderedRowColorMap: Map<string, string>,
|
|
|
+ ) => {
|
|
|
+ const row = leafColumns.map((col) => {
|
|
|
+ let value = record[col.dataIndex] ?? '';
|
|
|
+
|
|
|
+ // 应用 renderText 函数来格式化导出数据,保持与表格渲染时一致
|
|
|
+ if (col.renderText) {
|
|
|
+ value = col.renderText(value, record);
|
|
|
+ }
|
|
|
+
|
|
|
+ return value;
|
|
|
+ });
|
|
|
|
|
|
- const resp = await computeProfitReq(computeDate, currentTabKey);
|
|
|
- if (resp) {
|
|
|
- message.success('操作成功!');
|
|
|
- set_calcResultText('计算成功!');
|
|
|
- tableRef.current?.reload();
|
|
|
- } else {
|
|
|
- set_calcResultText('计算失败!');
|
|
|
- }
|
|
|
- },
|
|
|
- })
|
|
|
+ // 在第一列前添加缩进空格以表示层级
|
|
|
+ row[0] = ' '.repeat(level * 4) + row[0]; // 每一级增加 4 个空格作为缩进
|
|
|
+
|
|
|
+ worksheetData.push(row);
|
|
|
+ const rowKey = (record?.id ?? record?.key ?? '') as string | number;
|
|
|
+ const renderedRowColor =
|
|
|
+ rowKey !== ''
|
|
|
+ ? pickMeaningfulColor(renderedRowColorMap.get(String(rowKey)))
|
|
|
+ : undefined;
|
|
|
+ const explicitRowColor = pickMeaningfulColor(
|
|
|
+ convertToExcelRgb(getRawReportFontColor(record)),
|
|
|
+ );
|
|
|
+ dataRowColors.push(explicitRowColor || renderedRowColor);
|
|
|
+
|
|
|
+ // 递归处理子节点
|
|
|
+ if (record.children) {
|
|
|
+ record.children.forEach((child: any) =>
|
|
|
+ addRowWithIndentation(
|
|
|
+ child,
|
|
|
+ level + 1,
|
|
|
+ leafColumns,
|
|
|
+ worksheetData,
|
|
|
+ dataRowColors,
|
|
|
+ renderedRowColorMap,
|
|
|
+ ),
|
|
|
+ );
|
|
|
}
|
|
|
-
|
|
|
- const openTableDataDrawer = async () => {
|
|
|
- set_drawerTableVisible(true);
|
|
|
- set_loadingtransform(true);
|
|
|
- if (currentTab.value == '3') {
|
|
|
- const resp = await getReportDataReq(currentTabKey, computeDate, '3');
|
|
|
- if (resp) {
|
|
|
- const { title = [], data = [] } = resp;
|
|
|
- const defaultColumns: ProColumns[] = [
|
|
|
- {
|
|
|
- title: '报表项目名称',
|
|
|
- dataIndex: 'reportName',
|
|
|
- key: 'reportName',
|
|
|
- width: 220,
|
|
|
- fixed: 'left'
|
|
|
- }
|
|
|
- ];
|
|
|
-
|
|
|
- const tableColumns = generateTableColumns(title, 'responsibilityName');
|
|
|
- const dataSource = transformTreeData(data);
|
|
|
- set_tableColumns([...defaultColumns, ...tableColumns]);
|
|
|
- set_dataSource(dataSource);
|
|
|
- set_loadingtransform(false);
|
|
|
- // console.log({ columns: [...defaultColumns, ...tableColumns], dataSource })
|
|
|
-
|
|
|
- }
|
|
|
- } else {
|
|
|
- const resp = await getReportDataReq(currentTabKey, computeDate, currentTab.value);
|
|
|
- if (resp) {
|
|
|
- const { title = [], data = [] } = resp;
|
|
|
- const defaultColumns = [{
|
|
|
- title: '科室名称',
|
|
|
- dataIndex: 'responsibilityName',
|
|
|
- key: 'responsibilityName',
|
|
|
- width: 220,
|
|
|
- fixed: 'left'
|
|
|
- }];
|
|
|
-
|
|
|
- const tableColumns = generateTableColumns(title, 'reportName');
|
|
|
- const dataSource = processTree(data);
|
|
|
- set_tableColumns([...defaultColumns, ...tableColumns]);
|
|
|
-
|
|
|
- set_dataSource(dataSource);
|
|
|
- set_loadingtransform(false);
|
|
|
-
|
|
|
- // console.log({ columns: [...defaultColumns, ...tableColumns], dataSource })
|
|
|
-
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleExport = () => {
|
|
|
+ try {
|
|
|
+ const workbook = XLSX.utils.book_new();
|
|
|
+ const worksheetData: any[] = [];
|
|
|
+
|
|
|
+ // 获取最大层级
|
|
|
+ const maxLevel = tableColumns.reduce(
|
|
|
+ (max, col) => Math.max(max, getMaxLevel(col)),
|
|
|
+ 0,
|
|
|
+ );
|
|
|
+
|
|
|
+ // 生成多层级表头
|
|
|
+ const headerRows = getHeaderRows(tableColumns, 0, [], maxLevel);
|
|
|
+
|
|
|
+ // 构建表头行
|
|
|
+ headerRows.forEach((row: any, rowIndex) => {
|
|
|
+ const rowData: string[] = [];
|
|
|
+ row.forEach(
|
|
|
+ (cell: { title: any; colSpan: number; rowSpan: number }) => {
|
|
|
+ rowData.push(cell.title);
|
|
|
+ for (let i = 1; i < cell.colSpan; i++) {
|
|
|
+ rowData.push('');
|
|
|
}
|
|
|
+ },
|
|
|
+ );
|
|
|
+ worksheetData.push(rowData);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 填充单层表头的空白行
|
|
|
+ if (maxLevel > 1) {
|
|
|
+ const numColumns = headerRows[0].reduce(
|
|
|
+ (sum: any, cell: { colSpan: any }) => sum + cell.colSpan,
|
|
|
+ 0,
|
|
|
+ );
|
|
|
+ for (let i = 1; i < maxLevel; i++) {
|
|
|
+ while (worksheetData[i].length < numColumns) {
|
|
|
+ worksheetData[i].push('');
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
- const getHeaderRows = (columns: any[], level = 0, headerRows: any[] = [], maxLevel = 0) => {
|
|
|
- // 规则变更:将叶子列标题统一下沉至最底行(maxLevel - 1),
|
|
|
- // 上方各层以空白占位,避免纵向合并导致“高单元格”。
|
|
|
- headerRows[level] = headerRows[level] || [];
|
|
|
- columns.forEach((col: { title: any; children: any; }) => {
|
|
|
- const colSpan = getColSpan(col);
|
|
|
- if (col.children) {
|
|
|
- // 父节点仅进行横向合并,不做纵向合并
|
|
|
- headerRows[level].push({ title: col.title, colSpan, rowSpan: 1 });
|
|
|
- getHeaderRows(col.children, level + 1, headerRows, maxLevel);
|
|
|
- } else {
|
|
|
- // 叶子:从当前层到倒数第二层使用空白占位,叶子标题写入最底层
|
|
|
- for (let i = level; i < maxLevel - 1; i++) {
|
|
|
- headerRows[i] = headerRows[i] || [];
|
|
|
- headerRows[i].push({ title: '', colSpan: 1, rowSpan: 1 });
|
|
|
- }
|
|
|
- headerRows[maxLevel - 1] = headerRows[maxLevel - 1] || [];
|
|
|
- headerRows[maxLevel - 1].push({ title: col.title, colSpan: 1, rowSpan: 1 });
|
|
|
- }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取最内层表头列,并继承 title 树上的 fontColor(父级颜色可作用到整列)
|
|
|
+ const leafColumnsWithColor =
|
|
|
+ extractLeafColumnsWithInheritedColor(tableColumns);
|
|
|
+ const leafColumns = leafColumnsWithColor.map((item) => item.column);
|
|
|
+ const dataRowColors: Array<string | undefined> = [];
|
|
|
+ const columnColors: Array<string | undefined> = leafColumnsWithColor.map(
|
|
|
+ (item) => {
|
|
|
+ const dataIndexKey =
|
|
|
+ item?.column?.dataIndex !== undefined
|
|
|
+ ? String(item.column.dataIndex)
|
|
|
+ : '';
|
|
|
+ const titleColor = dataIndexKey
|
|
|
+ ? titleColumnColorMap[dataIndexKey]
|
|
|
+ : undefined;
|
|
|
+ return pickMeaningfulColor(
|
|
|
+ convertToExcelRgb(titleColor || item.color),
|
|
|
+ );
|
|
|
+ },
|
|
|
+ );
|
|
|
+ const renderedRowColorMap = collectRenderedRowColorMap();
|
|
|
+
|
|
|
+ // 添加数据并处理树结构
|
|
|
+ drawerDataSource.forEach((record) =>
|
|
|
+ addRowWithIndentation(
|
|
|
+ record,
|
|
|
+ 0,
|
|
|
+ leafColumns,
|
|
|
+ worksheetData,
|
|
|
+ dataRowColors,
|
|
|
+ renderedRowColorMap,
|
|
|
+ ),
|
|
|
+ );
|
|
|
+
|
|
|
+ const worksheet = XLSX.utils.aoa_to_sheet(worksheetData);
|
|
|
+
|
|
|
+ // 初始化合并单元格数组
|
|
|
+ worksheet['!merges'] = worksheet['!merges'] || [];
|
|
|
+
|
|
|
+ // 合并单元格
|
|
|
+ headerRows.forEach((row: any, rowIndex) => {
|
|
|
+ let colIndex = 0;
|
|
|
+ row.forEach((cell: { colSpan: number; rowSpan: number }) => {
|
|
|
+ if (cell.colSpan > 1 || cell.rowSpan > 1) {
|
|
|
+ worksheet['!merges']!.push({
|
|
|
+ // 使用非空断言 '!'
|
|
|
+ s: { r: rowIndex, c: colIndex },
|
|
|
+ e: {
|
|
|
+ r: rowIndex + cell.rowSpan - 1,
|
|
|
+ c: colIndex + cell.colSpan - 1,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+ colIndex += cell.colSpan;
|
|
|
});
|
|
|
- return headerRows;
|
|
|
- };
|
|
|
-
|
|
|
- const getColSpan: any = (col: { children: any[]; }) => {
|
|
|
- if (!col.children) return 1;
|
|
|
- return col.children.reduce((sum, child) => sum + getColSpan(child), 0);
|
|
|
- };
|
|
|
-
|
|
|
- const getMaxLevel = (col: any) => {
|
|
|
- if (!col.children) return 1;
|
|
|
- return 1 + Math.max(...col.children.map(getMaxLevel));
|
|
|
- };
|
|
|
-
|
|
|
- const extractLeafColumns = (columns: any[]) => {
|
|
|
- let leafColumns: any[] = [];
|
|
|
- columns.forEach(col => {
|
|
|
- if (col.children) {
|
|
|
- leafColumns = leafColumns.concat(extractLeafColumns(col.children));
|
|
|
- } else {
|
|
|
- leafColumns.push(col);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 设置单元格对齐方式
|
|
|
+ Object.keys(worksheet).forEach((cell) => {
|
|
|
+ if (cell[0] !== '!') {
|
|
|
+ worksheet[cell].s = {
|
|
|
+ alignment: { vertical: 'center', horizontal: 'center' },
|
|
|
+ };
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 表头加粗:对 headerRows 对应的单元格区域(含横向合并范围)统一设置加粗
|
|
|
+ headerRows.forEach((row: any, rowIndex: number) => {
|
|
|
+ let colIndex = 0;
|
|
|
+ row.forEach((cell: { colSpan: number; rowSpan?: number }) => {
|
|
|
+ const spanCols = cell.colSpan || 1;
|
|
|
+ const spanRows = cell.rowSpan || 1;
|
|
|
+ for (let r = rowIndex; r < rowIndex + spanRows; r++) {
|
|
|
+ for (let c = colIndex; c < colIndex + spanCols; c++) {
|
|
|
+ const cellRef = XLSX.utils.encode_cell({ r, c });
|
|
|
+ if (!worksheet[cellRef]) {
|
|
|
+ (worksheet as any)[cellRef] = { t: 's', v: '' };
|
|
|
+ }
|
|
|
+ const prevStyle = (worksheet as any)[cellRef].s || {};
|
|
|
+ (worksheet as any)[cellRef].s = {
|
|
|
+ ...prevStyle,
|
|
|
+ font: {
|
|
|
+ name: '微软雅黑',
|
|
|
+ sz: 11,
|
|
|
+ ...(prevStyle.font || {}),
|
|
|
+ bold: true,
|
|
|
+ },
|
|
|
+ alignment: prevStyle.alignment || {
|
|
|
+ vertical: 'center',
|
|
|
+ horizontal: 'center',
|
|
|
+ },
|
|
|
+ };
|
|
|
}
|
|
|
+ }
|
|
|
+ colIndex += spanCols;
|
|
|
});
|
|
|
- return leafColumns;
|
|
|
- };
|
|
|
-
|
|
|
- const addRowWithIndentation = (record: any, level: number, leafColumns: any[], worksheetData: any[]) => {
|
|
|
- const row = leafColumns.map(col => {
|
|
|
- let value = record[col.dataIndex] ?? '';
|
|
|
-
|
|
|
- // 应用 renderText 函数来格式化导出数据,保持与表格渲染时一致
|
|
|
- if (col.renderText) {
|
|
|
- value = col.renderText(value, record);
|
|
|
- }
|
|
|
-
|
|
|
- return value;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 表头字体颜色:按叶子列的颜色映射到最底层表头(与列颜色保持一致)
|
|
|
+ const headerRowsCount = headerRows.length;
|
|
|
+ const leafHeaderRowIndex = Math.max(0, headerRowsCount - 1);
|
|
|
+ for (let colIndex = 0; colIndex < leafColumns.length; colIndex++) {
|
|
|
+ const cellColor = columnColors[colIndex];
|
|
|
+ if (!cellColor) continue;
|
|
|
+ const excelColor =
|
|
|
+ cellColor.length === 6 ? `FF${cellColor}` : cellColor;
|
|
|
+ const cellRef = XLSX.utils.encode_cell({
|
|
|
+ r: leafHeaderRowIndex,
|
|
|
+ c: colIndex,
|
|
|
});
|
|
|
-
|
|
|
- // 在第一列前添加缩进空格以表示层级
|
|
|
- row[0] = ' '.repeat(level * 4) + row[0]; // 每一级增加 4 个空格作为缩进
|
|
|
-
|
|
|
- worksheetData.push(row);
|
|
|
-
|
|
|
- // 递归处理子节点
|
|
|
- if (record.children) {
|
|
|
- record.children.forEach((child: any) => addRowWithIndentation(child, level + 1, leafColumns, worksheetData));
|
|
|
+ if (!worksheet[cellRef]) continue;
|
|
|
+ const prevStyle = worksheet[cellRef].s || {};
|
|
|
+ worksheet[cellRef].s = {
|
|
|
+ ...prevStyle,
|
|
|
+ font: {
|
|
|
+ name: '微软雅黑',
|
|
|
+ sz: 11,
|
|
|
+ ...(prevStyle.font || {}),
|
|
|
+ bold: true,
|
|
|
+ color: { rgb: excelColor },
|
|
|
+ },
|
|
|
+ alignment: prevStyle.alignment || {
|
|
|
+ vertical: 'center',
|
|
|
+ horizontal: 'center',
|
|
|
+ },
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ dataRowColors.forEach((rowColor, index) => {
|
|
|
+ const rowIndex = headerRowsCount + index;
|
|
|
+ for (let colIndex = 0; colIndex < leafColumns.length; colIndex++) {
|
|
|
+ const cellColor = rowColor || columnColors[colIndex];
|
|
|
+ if (!cellColor) continue;
|
|
|
+ const excelColor =
|
|
|
+ cellColor.length === 6 ? `FF${cellColor}` : cellColor;
|
|
|
+ const cellRef = XLSX.utils.encode_cell({ r: rowIndex, c: colIndex });
|
|
|
+ if (!worksheet[cellRef]) continue;
|
|
|
+ const prevStyle = worksheet[cellRef].s || {};
|
|
|
+ worksheet[cellRef].s = {
|
|
|
+ ...prevStyle,
|
|
|
+ font: {
|
|
|
+ name: '微软雅黑',
|
|
|
+ sz: 11,
|
|
|
+ ...(prevStyle.font || {}),
|
|
|
+ color: { rgb: excelColor },
|
|
|
+ },
|
|
|
+ alignment: prevStyle.alignment || {
|
|
|
+ vertical: 'center',
|
|
|
+ horizontal: 'center',
|
|
|
+ },
|
|
|
+ };
|
|
|
}
|
|
|
- };
|
|
|
+ });
|
|
|
|
|
|
- const handleExport = () => {
|
|
|
- try {
|
|
|
- const workbook = XLSX.utils.book_new();
|
|
|
- const worksheetData: any[] = [];
|
|
|
-
|
|
|
- // 获取最大层级
|
|
|
- const maxLevel = tableColumns.reduce((max, col) => Math.max(max, getMaxLevel(col)), 0);
|
|
|
-
|
|
|
- // 生成多层级表头
|
|
|
- const headerRows = getHeaderRows(tableColumns, 0, [], maxLevel);
|
|
|
-
|
|
|
- // 构建表头行
|
|
|
- headerRows.forEach((row: any, rowIndex) => {
|
|
|
- const rowData: string[] = [];
|
|
|
- row.forEach((cell: { title: any; colSpan: number; rowSpan: number; }) => {
|
|
|
- rowData.push(cell.title);
|
|
|
- for (let i = 1; i < cell.colSpan; i++) {
|
|
|
- rowData.push('');
|
|
|
- }
|
|
|
- });
|
|
|
- worksheetData.push(rowData);
|
|
|
- });
|
|
|
-
|
|
|
- // 填充单层表头的空白行
|
|
|
- if (maxLevel > 1) {
|
|
|
- const numColumns = headerRows[0].reduce((sum: any, cell: { colSpan: any; }) => sum + cell.colSpan, 0);
|
|
|
- for (let i = 1; i < maxLevel; i++) {
|
|
|
- while (worksheetData[i].length < numColumns) {
|
|
|
- worksheetData[i].push('');
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 提取最内层表头列
|
|
|
- const leafColumns = extractLeafColumns(tableColumns);
|
|
|
-
|
|
|
- // 添加数据并处理树结构
|
|
|
- dataSource.forEach(record => addRowWithIndentation(record, 0, leafColumns, worksheetData));
|
|
|
+ XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
|
|
|
|
|
- const worksheet = XLSX.utils.aoa_to_sheet(worksheetData);
|
|
|
-
|
|
|
- // 初始化合并单元格数组
|
|
|
- worksheet['!merges'] = worksheet['!merges'] || [];
|
|
|
-
|
|
|
- // 合并单元格
|
|
|
- headerRows.forEach((row: any, rowIndex) => {
|
|
|
- let colIndex = 0;
|
|
|
- row.forEach((cell: { colSpan: number; rowSpan: number; }) => {
|
|
|
- if (cell.colSpan > 1 || cell.rowSpan > 1) {
|
|
|
- worksheet['!merges']!.push({ // 使用非空断言 '!'
|
|
|
- s: { r: rowIndex, c: colIndex },
|
|
|
- e: { r: rowIndex + cell.rowSpan - 1, c: colIndex + cell.colSpan - 1 }
|
|
|
- });
|
|
|
- }
|
|
|
- colIndex += cell.colSpan;
|
|
|
- });
|
|
|
- });
|
|
|
-
|
|
|
- // 设置单元格对齐方式
|
|
|
- Object.keys(worksheet).forEach(cell => {
|
|
|
- if (cell[0] !== '!') {
|
|
|
- worksheet[cell].s = {
|
|
|
- alignment: { vertical: 'center', horizontal: 'center' }
|
|
|
- };
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 表头加粗:对 headerRows 对应的单元格区域(含横向合并范围)统一设置加粗
|
|
|
- headerRows.forEach((row: any, rowIndex: number) => {
|
|
|
- let colIndex = 0;
|
|
|
- row.forEach((cell: { colSpan: number; rowSpan?: number }) => {
|
|
|
- const spanCols = cell.colSpan || 1;
|
|
|
- const spanRows = cell.rowSpan || 1;
|
|
|
- for (let r = rowIndex; r < rowIndex + spanRows; r++) {
|
|
|
- for (let c = colIndex; c < colIndex + spanCols; c++) {
|
|
|
- const cellRef = XLSX.utils.encode_cell({ r, c });
|
|
|
- if (!worksheet[cellRef]) {
|
|
|
- (worksheet as any)[cellRef] = { t: 's', v: '' };
|
|
|
- }
|
|
|
- const prevStyle = (worksheet as any)[cellRef].s || {};
|
|
|
- (worksheet as any)[cellRef].s = {
|
|
|
- ...prevStyle,
|
|
|
- font: { name: '微软雅黑', sz: 11, ...(prevStyle.font || {}), bold: true },
|
|
|
- alignment: prevStyle.alignment || { vertical: 'center', horizontal: 'center' }
|
|
|
- };
|
|
|
- }
|
|
|
- }
|
|
|
- colIndex += spanCols;
|
|
|
- });
|
|
|
- });
|
|
|
-
|
|
|
- XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
|
|
-
|
|
|
- // 使用库自带的 writeFile 以最大化样式兼容性
|
|
|
- XLSX.writeFile(workbook, currentTab ? `${currentTab.label}.xlsx` : 'table_data.xlsx');
|
|
|
- } catch (error) {
|
|
|
- console.error('Export failed:', error);
|
|
|
+ // 使用 write + saveAs,避免浏览器环境下 writeFile 的样式兼容差异
|
|
|
+ const wbout = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
|
|
|
+ const blob = new Blob([wbout], { type: 'application/octet-stream' });
|
|
|
+ saveAs(blob, currentTab ? `${currentTab.label}.xlsx` : 'table_data.xlsx');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Export failed:', error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleExpandNext = useCallback(() => {
|
|
|
+ set_allParentsKeys((prev) => {
|
|
|
+ const keysToExpand = getNextUnexpandedKeys(
|
|
|
+ mainTableDataSource,
|
|
|
+ new Set(prev),
|
|
|
+ );
|
|
|
+ if (keysToExpand.length === 0) return prev;
|
|
|
+ return Array.from(new Set([...prev, ...keysToExpand]));
|
|
|
+ });
|
|
|
+ }, [mainTableDataSource]);
|
|
|
+
|
|
|
+ const handleCollapseAll = useCallback(() => {
|
|
|
+ set_allParentsKeys([]);
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const handleMainTableExpand = useCallback(
|
|
|
+ (expanded: boolean, record: any) => {
|
|
|
+ const { id } = record;
|
|
|
+ set_allParentsKeys((prev) => {
|
|
|
+ if (!expanded) {
|
|
|
+ return prev.filter((key) => key != id);
|
|
|
}
|
|
|
- };
|
|
|
-
|
|
|
-
|
|
|
- const handleExpandNext = () => {
|
|
|
- // 当前所有未展开的节点,第一层优先展开
|
|
|
- const keysToExpand = getNextUnexpandedKeys(dataSource, allParentsKeys);
|
|
|
- set_allParentsKeys((prev) => Array.from(new Set([...prev, ...keysToExpand])));
|
|
|
- };
|
|
|
+ if (prev.includes(id)) {
|
|
|
+ return prev;
|
|
|
+ }
|
|
|
+ return [...prev, id];
|
|
|
+ });
|
|
|
+ },
|
|
|
+ [],
|
|
|
+ );
|
|
|
+
|
|
|
+ const mainTableRowClassName = useCallback(
|
|
|
+ (record: any) =>
|
|
|
+ `${
|
|
|
+ record.children ? 'has-children ' : ''
|
|
|
+ }hover-row report-font-color-row`,
|
|
|
+ [],
|
|
|
+ );
|
|
|
+
|
|
|
+ const drawerTableRowClassName = useCallback(
|
|
|
+ (record: any) =>
|
|
|
+ `${record.children ? 'has-children ' : ''}report-font-color-row`,
|
|
|
+ [],
|
|
|
+ );
|
|
|
+
|
|
|
+ const emptyRowProps = useMemo(() => ({}), []);
|
|
|
+
|
|
|
+ const getReportRowProps = useCallback(
|
|
|
+ (record: any) => {
|
|
|
+ const color = getRawReportFontColor(record);
|
|
|
+ if (!color) return emptyRowProps;
|
|
|
+ return {
|
|
|
+ style: {
|
|
|
+ ['--report-font-color' as any]: color,
|
|
|
+ } as React.CSSProperties,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ [emptyRowProps],
|
|
|
+ );
|
|
|
+
|
|
|
+ const handleDrawerExpand = useCallback((expanded: boolean, record: any) => {
|
|
|
+ const { id } = record;
|
|
|
+ set_drawerExpandedKeys((prev) => {
|
|
|
+ if (!expanded) {
|
|
|
+ return prev.filter((key) => key != id);
|
|
|
+ }
|
|
|
+ if (prev.includes(id)) {
|
|
|
+ return prev;
|
|
|
+ }
|
|
|
+ return [...prev, id];
|
|
|
+ });
|
|
|
+ }, []);
|
|
|
|
|
|
- const handleCollapseAll = () => {
|
|
|
- set_allParentsKeys([]);
|
|
|
+ useEffect(() => {
|
|
|
+ if (computeDate && currentTabKey != undefined) {
|
|
|
+ getResponsibleCenterList(currentTabKey);
|
|
|
+ }
|
|
|
+ set_allParentsKeys([]);
|
|
|
+ set_mainTableDataSource([]);
|
|
|
+ set_drawerDataSource([]);
|
|
|
+ set_drawerExpandedKeys([]);
|
|
|
+ }, [computeDate, currentTabKey]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (currentSelectedRespon) {
|
|
|
+ set_tableDataFilterParams((prev: any) => ({
|
|
|
+ ...prev,
|
|
|
+ responsibilityCode: currentSelectedRespon.responsibilityCode,
|
|
|
+ reportType: currentTabKey,
|
|
|
+ computeDate: computeDate,
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ set_allParentsKeys([]);
|
|
|
+ set_mainTableDataSource([]);
|
|
|
+ set_drawerDataSource([]);
|
|
|
+ set_drawerExpandedKeys([]);
|
|
|
+ }, [computeDate, currentSelectedRespon, currentTabKey]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ getTabs();
|
|
|
+ getIfshowPercent();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const drawerTableScroll = useMemo(() => {
|
|
|
+ return {
|
|
|
+ x: countLeafNodes(tableColumns) * 200,
|
|
|
+ y: `calc(100vh - ${
|
|
|
+ 84 +
|
|
|
+ tableColumns.reduce((max, col) => Math.max(max, getMaxLevel(col)), 0) *
|
|
|
+ 38
|
|
|
+ }px)`,
|
|
|
};
|
|
|
-
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- if (computeDate && currentTabKey != undefined) {
|
|
|
- getResponsibleCenterList(currentTabKey);
|
|
|
- }
|
|
|
- set_allParentsKeys([]);
|
|
|
- set_dataSource([]);
|
|
|
- }, [computeDate, currentTabKey]);
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- if (currentSelectedRespon) {
|
|
|
- set_tableDataFilterParams({
|
|
|
- ...tableDataFilterParams,
|
|
|
- responsibilityCode: currentSelectedRespon.responsibilityCode,
|
|
|
- reportType: currentTabKey,
|
|
|
- computeDate: computeDate
|
|
|
- })
|
|
|
- }
|
|
|
- set_allParentsKeys([]);
|
|
|
- set_dataSource([]);
|
|
|
- }, [currentSelectedRespon])
|
|
|
-
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- getTabs();
|
|
|
- getIfshowPercent();
|
|
|
- }, [])
|
|
|
-
|
|
|
- return (
|
|
|
- <KCIMPagecontainer className='DepartmentCostCalc' title={false}>
|
|
|
-
|
|
|
- <Drawer className='drawerTable' contentWrapperStyle={{}} bodyStyle={{ padding: 16 }} title={false} open={drawerTableVisible} width={1000} headerStyle={{ display: 'none' }}>
|
|
|
- <div className='header'>
|
|
|
- <div className='title'>{currentTab ? currentTab.label : ''}(单位:元)</div>
|
|
|
-
|
|
|
- <div className='btns'>
|
|
|
- <span onClick={() => set_drawerTableVisible(false)}>关闭</span>
|
|
|
- <span className='close' onClick={() => handleExport()}>导出</span>
|
|
|
-
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- {/* {loadingtransform && <Skeleton />} */}
|
|
|
- <KCIMTable loading={loadingtransform} expandable={{ defaultExpandAllRows: true }} className='departmentCostCalcReportTable'
|
|
|
- dataSource={dataSource} bordered pagination={false}
|
|
|
- rowClassName={(record) => `${record.children ? 'has-children ' : ''}report-font-color-row`}
|
|
|
- onRow={(record: any) => ({
|
|
|
- style: {
|
|
|
- ['--report-font-color' as any]: getReportFontColor(record),
|
|
|
- } as React.CSSProperties,
|
|
|
- })}
|
|
|
- scroll={{ x: countLeafNodes(tableColumns) * 200, y: `calc(100vh - ${84 + ((tableColumns.reduce((max, col) => Math.max(max, getMaxLevel(col)), 0)) * 38)}px)` }}
|
|
|
- columns={tableColumns as ProColumns[]} rowKey='id' />
|
|
|
-
|
|
|
- </Drawer>
|
|
|
-
|
|
|
- <div className='header'>
|
|
|
- <div className="search">
|
|
|
- <span>核算年月:</span>
|
|
|
- <DatePicker
|
|
|
- onChange={(data, dateString) => {
|
|
|
- set_computeDate(dateString);
|
|
|
- setInitialState((s: any) => ({ ...s, computeDate: dateString, }))
|
|
|
- set_tableDataFilterParams({
|
|
|
- ...tableDataFilterParams,
|
|
|
- computeDate: dateString,
|
|
|
- });
|
|
|
- }}
|
|
|
- picker="month"
|
|
|
- locale={locale}
|
|
|
- defaultValue={moment(computeDate, 'YYYY-MM')}
|
|
|
- format="YYYY-MM"
|
|
|
- autoComplete="off"
|
|
|
- placeholder="选择年月"
|
|
|
+ }, [tableColumns]);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <KCIMPagecontainer className="DepartmentCostCalc" title={false}>
|
|
|
+ <Drawer
|
|
|
+ className="drawerTable"
|
|
|
+ contentWrapperStyle={{}}
|
|
|
+ bodyStyle={{ padding: 16 }}
|
|
|
+ title={false}
|
|
|
+ open={drawerTableVisible}
|
|
|
+ width={1000}
|
|
|
+ headerStyle={{ display: 'none' }}
|
|
|
+ >
|
|
|
+ <div className="header">
|
|
|
+ <div className="title">
|
|
|
+ {currentTab ? currentTab.label : ''}(单位:元)
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="btns">
|
|
|
+ <span onClick={() => set_drawerTableVisible(false)}>关闭</span>
|
|
|
+ <span className="close" onClick={() => handleExport()}>
|
|
|
+ 导出
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {/* {loadingtransform && <Skeleton />} */}
|
|
|
+ <KCIMTable
|
|
|
+ loading={loadingtransform}
|
|
|
+ expandable={{
|
|
|
+ defaultExpandedRowKeys: drawerExpandedKeys,
|
|
|
+ }}
|
|
|
+ components={drawerVirtualComponents}
|
|
|
+ className="departmentCostCalcReportTable"
|
|
|
+ key={drawerDataSource.length ? 'loaded' : 'empty'}
|
|
|
+ dataSource={drawerDataSource}
|
|
|
+ bordered
|
|
|
+ pagination={false}
|
|
|
+ rowClassName={drawerTableRowClassName}
|
|
|
+ onRow={getReportRowProps}
|
|
|
+ scroll={drawerTableScroll}
|
|
|
+ columns={tableColumns as ProColumns[]}
|
|
|
+ rowKey="id"
|
|
|
+ />
|
|
|
+ </Drawer>
|
|
|
+
|
|
|
+ <div className="header">
|
|
|
+ <div className="search">
|
|
|
+ <span>核算年月:</span>
|
|
|
+ <DatePicker
|
|
|
+ onChange={(data, dateString) => {
|
|
|
+ set_computeDate(dateString);
|
|
|
+ setInitialState((s: any) => ({ ...s, computeDate: dateString }));
|
|
|
+ set_tableDataFilterParams((prev: any) => ({
|
|
|
+ ...prev,
|
|
|
+ computeDate: dateString,
|
|
|
+ }));
|
|
|
+ }}
|
|
|
+ picker="month"
|
|
|
+ locale={locale}
|
|
|
+ defaultValue={moment(computeDate, 'YYYY-MM')}
|
|
|
+ format="YYYY-MM"
|
|
|
+ autoComplete="off"
|
|
|
+ placeholder="选择年月"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="btnGoup">
|
|
|
+ <span
|
|
|
+ className="onekeyCalcBtn"
|
|
|
+ onClick={() => onekeyComputeProfitHandle()}
|
|
|
+ >
|
|
|
+ 一键计算
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div className="content">
|
|
|
+ <Tabs
|
|
|
+ defaultActiveKey={tabs.length > 0 ? tabs[0].key : undefined}
|
|
|
+ items={tabs}
|
|
|
+ key={'key'}
|
|
|
+ onChange={(key) => onTabChanged(key)}
|
|
|
+ />
|
|
|
+ {calcResultText && (
|
|
|
+ <Alert
|
|
|
+ showIcon
|
|
|
+ onClose={() => set_calcResultText(undefined)}
|
|
|
+ icon={
|
|
|
+ <IconFont
|
|
|
+ type={
|
|
|
+ calcResultText.indexOf('失败') != -1
|
|
|
+ ? 'icon-cuowutishi'
|
|
|
+ : 'icon-chenggongtishi'
|
|
|
+ }
|
|
|
+ />
|
|
|
+ }
|
|
|
+ closable
|
|
|
+ style={{
|
|
|
+ padding: '4px 12px',
|
|
|
+ marginBottom: 16,
|
|
|
+ borderRadius: 4,
|
|
|
+ border:
|
|
|
+ calcResultText.indexOf('失败') != -1 ? '1px solid #73E6BF' : '',
|
|
|
+ background:
|
|
|
+ calcResultText.indexOf('失败') != -1 ? '#FFF1F3' : '#EBFFF8',
|
|
|
+ }}
|
|
|
+ message={calcResultText}
|
|
|
+ type={calcResultText.indexOf('失败') != -1 ? 'error' : 'success'}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ <div className="inner">
|
|
|
+ <div className="left">
|
|
|
+ <KCIMLeftList
|
|
|
+ fieldNames={{
|
|
|
+ title: 'responsibilityName',
|
|
|
+ key: 'responsibilityCode',
|
|
|
+ children: 'children',
|
|
|
+ }}
|
|
|
+ rowKey={'responsibilityCode'}
|
|
|
+ dataSource={responsibleCenters}
|
|
|
+ searchKey={'responsibilityName'}
|
|
|
+ onChange={onLeftChange}
|
|
|
+ contentH={`100%`}
|
|
|
+ // placeholder={leftListSearchPlaceHolder}
|
|
|
+ listType={'tree'}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="right">
|
|
|
+ <div className="toolBar">
|
|
|
+ <div className="filterItem" style={{ width: 228 }}>
|
|
|
+ <span className="label" style={{ whiteSpace: 'nowrap' }}>
|
|
|
+ {' '}
|
|
|
+ 检索:
|
|
|
+ </span>
|
|
|
+ <Input
|
|
|
+ placeholder={'报表项目代码/名称'}
|
|
|
+ allowClear
|
|
|
+ autoComplete="off"
|
|
|
+ suffix={
|
|
|
+ <IconFont
|
|
|
+ type="iconsousuo"
|
|
|
+ style={{ color: '#99A6BF' }}
|
|
|
+ onClick={() => tableDataSearchHandle('filter')}
|
|
|
/>
|
|
|
- </div>
|
|
|
- <div className='btnGoup'>
|
|
|
- <span className='onekeyCalcBtn' onClick={() => onekeyComputeProfitHandle()}>一键计算</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div className='content'>
|
|
|
- <Tabs
|
|
|
- defaultActiveKey={tabs.length > 0 ? tabs[0].key : undefined}
|
|
|
- items={tabs}
|
|
|
- key={'key'}
|
|
|
- onChange={(key) => onTabChanged(key)}
|
|
|
+ }
|
|
|
+ onChange={(e) => {
|
|
|
+ set_tableDataSearchKeywords(e.target.value);
|
|
|
+ if (e.target.value.length == 0) {
|
|
|
+ set_tableDataFilterParams((prev: any) => ({
|
|
|
+ ...prev,
|
|
|
+ filter: '',
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ onPressEnter={(e) => {
|
|
|
+ set_tableDataFilterParams((prev: any) => ({
|
|
|
+ ...prev,
|
|
|
+ filter: (e.target as HTMLInputElement).value,
|
|
|
+ }));
|
|
|
+ }}
|
|
|
/>
|
|
|
- {calcResultText && <Alert showIcon onClose={() => set_calcResultText(undefined)} icon={<IconFont type={(calcResultText.indexOf('失败') != -1) ? 'icon-cuowutishi' : 'icon-chenggongtishi'} />} closable style={{ padding: '4px 12px', marginBottom: 16, borderRadius: 4, border: (calcResultText.indexOf('失败') != -1) ? '1px solid #73E6BF' : '', background: (calcResultText.indexOf('失败') != -1) ? '#FFF1F3' : '#EBFFF8' }} message={calcResultText} type={calcResultText.indexOf('失败') != -1 ? 'error' : 'success'} />}
|
|
|
- <div className='inner'>
|
|
|
- <div className='left'>
|
|
|
- <KCIMLeftList
|
|
|
- fieldNames={{ title: 'responsibilityName', key: 'responsibilityCode', children: 'children' }}
|
|
|
- rowKey={'responsibilityCode'}
|
|
|
- dataSource={responsibleCenters} searchKey={'responsibilityName'}
|
|
|
- onChange={onLeftChange}
|
|
|
- contentH={`100%`}
|
|
|
- // placeholder={leftListSearchPlaceHolder}
|
|
|
- listType={'tree'}
|
|
|
-
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div className='right'>
|
|
|
- <div className='toolBar'>
|
|
|
- <div className='filterItem' style={{ width: 228 }}>
|
|
|
- <span className='label' style={{ whiteSpace: 'nowrap' }}> 检索:</span>
|
|
|
- <Input placeholder={'报表项目代码/名称'} allowClear autoComplete='off'
|
|
|
- suffix={
|
|
|
- <IconFont type="iconsousuo" style={{ color: '#99A6BF' }} onClick={() => tableDataSearchHandle('filter')} />
|
|
|
- }
|
|
|
- onChange={(e) => {
|
|
|
- set_tableDataSearchKeywords(e.target.value);
|
|
|
- if (e.target.value.length == 0) {
|
|
|
- set_tableDataFilterParams({
|
|
|
- ...tableDataFilterParams,
|
|
|
- filter: ''
|
|
|
- });
|
|
|
- }
|
|
|
- }}
|
|
|
- onPressEnter={(e) => {
|
|
|
- set_tableDataFilterParams({
|
|
|
- ...tableDataFilterParams,
|
|
|
- filter: ((e.target) as HTMLInputElement).value
|
|
|
- });
|
|
|
- }}
|
|
|
-
|
|
|
- />
|
|
|
- </div>
|
|
|
- <div className='btnGroup'>
|
|
|
- <span className='btn' onClick={() => handleCollapseAll()}>全部折叠</span>
|
|
|
- <span className='btn' style={{ marginRight: 16 }} onClick={() => handleExpandNext()}>展开下一层</span>
|
|
|
- <span className='btn' onClick={() => openTableDataDrawer()}>报表数据</span>
|
|
|
- <span className='calc' onClick={() => computeProfitHandle()}>计算</span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <KCIMTable pagination={false}
|
|
|
- rowClassName={(record) => `${record.children ? 'has-children ' : ''}hover-row report-font-color-row`}
|
|
|
- onRow={(record: any) => ({
|
|
|
- style: {
|
|
|
- ['--report-font-color' as any]: getReportFontColor(record),
|
|
|
- } as React.CSSProperties,
|
|
|
- })}
|
|
|
- expandable={{
|
|
|
- expandedRowKeys: allParentsKeys,
|
|
|
- onExpand(expanded, record) {
|
|
|
- const { id } = record;
|
|
|
- if (!expanded) {
|
|
|
- const expandedKeys = allParentsKeys.filter(a => a != id);
|
|
|
- set_allParentsKeys([...expandedKeys]);
|
|
|
- } else {
|
|
|
- set_allParentsKeys([...allParentsKeys, id]);
|
|
|
- }
|
|
|
- },
|
|
|
- }} columns={columns as ProColumns[]} scroll={{ y: `calc(100vh - 302px)` }} actionRef={tableRef} rowKey='id' params={tableDataFilterParams} request={(params) => getTableData(params)} />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
-
|
|
|
+ </div>
|
|
|
+ <div className="btnGroup">
|
|
|
+ <span className="btn" onClick={() => handleCollapseAll()}>
|
|
|
+ 全部折叠
|
|
|
+ </span>
|
|
|
+ <span
|
|
|
+ className="btn"
|
|
|
+ style={{ marginRight: 16 }}
|
|
|
+ onClick={() => handleExpandNext()}
|
|
|
+ >
|
|
|
+ 展开下一层
|
|
|
+ </span>
|
|
|
+ <span className="btn" onClick={() => openTableDataDrawer()}>
|
|
|
+ 报表数据
|
|
|
+ </span>
|
|
|
+ <span className="calc" onClick={() => computeProfitHandle()}>
|
|
|
+ 计算
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </KCIMPagecontainer>
|
|
|
- )
|
|
|
+ <KCIMTable
|
|
|
+ pagination={false}
|
|
|
+ rowClassName={mainTableRowClassName}
|
|
|
+ onRow={getReportRowProps}
|
|
|
+ expandable={{
|
|
|
+ expandedRowKeys: allParentsKeys,
|
|
|
+ onExpand: handleMainTableExpand,
|
|
|
+ }}
|
|
|
+ columns={columns as ProColumns[]}
|
|
|
+ scroll={{ y: `calc(100vh - 302px)` }}
|
|
|
+ actionRef={tableRef}
|
|
|
+ rowKey="id"
|
|
|
+ params={tableDataFilterParams}
|
|
|
+ request={(params) => getTableData(params)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </KCIMPagecontainer>
|
|
|
+ );
|
|
|
}
|
|
|
-
|