Kaynağa Gözat

feat(costAccounting): 成本核算模板与相关页面优化

- calcPageTemplate:抽离筛选栏/权限/导出逻辑,补充配置与样式调整

- 科室成本校验与月度采集、报表导出、特殊数据导入:交互与接口补充

- 新增 hospitalProfitAndLoss 页面
code4eat 3 hafta önce
ebeveyn
işleme
86bee1431a
33 değiştirilmiş dosya ile 3115 ekleme ve 492 silme
  1. 54 17
      .umirc.ts
  2. 5 2
      package.json
  3. 25 18
      src/app.tsx
  4. 22 4
      src/components/KCIMLeftList/index.tsx
  5. 2 1
      src/components/KCIMLeftList/style.less
  6. 58 35
      src/components/KCIMTable/style.less
  7. 25 13
      src/global.less
  8. 37 14
      src/pages/baseSetting/otherItemSet/departmentCostCalc/index.tsx
  9. 127 0
      src/pages/costAccounting/calcPageTemplate/FilterBar.tsx
  10. 25 31
      src/pages/costAccounting/calcPageTemplate/columns.tsx
  11. 59 0
      src/pages/costAccounting/calcPageTemplate/config.ts
  12. 251 154
      src/pages/costAccounting/calcPageTemplate/index.tsx
  13. 3 3
      src/pages/costAccounting/calcPageTemplate/service.ts
  14. 44 17
      src/pages/costAccounting/calcPageTemplate/strategy.tsx
  15. 9 5
      src/pages/costAccounting/calcPageTemplate/style.less
  16. 16 0
      src/pages/costAccounting/calcPageTemplate/usePageAccess.ts
  17. 37 0
      src/pages/costAccounting/calcPageTemplate/usePagedExporter.ts
  18. 576 70
      src/pages/departmentCostCheck/index.tsx
  19. 8 0
      src/pages/departmentCostCheck/service.ts
  20. 2 2
      src/pages/departmentMenzhuCostCalc/index.tsx
  21. 1031 0
      src/pages/hospitalProfitAndLoss/index.tsx
  22. 18 0
      src/pages/hospitalProfitAndLoss/service.ts
  23. 110 0
      src/pages/hospitalProfitAndLoss/style.less
  24. 68 15
      src/pages/monthlyInfoCollection/components/leftAndRighrStructure.tsx
  25. 3 3
      src/pages/monthlyInfoCollection/index.tsx
  26. 3 3
      src/pages/monthlyInfoCollection/service.ts
  27. 267 47
      src/pages/reportExport/report/index.tsx
  28. 114 26
      src/pages/specialDataImport/index.tsx
  29. 9 0
      src/pages/specialDataImport/service.ts
  30. 1 1
      src/services/getDic.ts
  31. 39 9
      src/utils/exporter.ts
  32. 4 1
      typings.d.ts
  33. 63 1
      yarn.lock

+ 54 - 17
.umirc.ts

@@ -14,10 +14,20 @@ import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin';
 const { REACT_APP_ENV } = process.env;
 const path = require('path');
 
+// 从 package.json 读取版本号,如果没有则使用默认值
+const pkg = require('./package.json');
+const APP_VERSION = pkg.version || '1.0.0';
+const BUILD_TIME = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
+
 
 
 export default defineConfig({
-  hash:true,
+  // 注入版本信息到全局变量
+  define: {
+    'process.env.APP_VERSION': APP_VERSION,
+    'process.env.BUILD_TIME': BUILD_TIME,
+  },
+  hash: true,
   antd: {
     configProvider: {
       prefixCls: 'cost-ant',
@@ -41,10 +51,10 @@ export default defineConfig({
 
   chainWebpack(config, { webpack }) {
     config.plugin('monaco-editor').use(MonacoWebpackPlugin, [
-        {
-            // 指定需要加载的语言和特性
-            languages: ['sql', 'javascript', 'typescript']
-        }
+      {
+        // 指定需要加载的语言和特性
+        languages: ['sql', 'javascript', 'typescript']
+      }
     ]);
   },
 
@@ -135,7 +145,7 @@ export default defineConfig({
         },
         {
           path: '/baseSetting/costAllocationSet',
-          
+
           name: '成本分摊设置',
           routes: [
             {
@@ -205,29 +215,56 @@ export default defineConfig({
       ],
     },
     {
-      path:'/monthlyInfoCollection',
+      path: '/monthlyInfoCollection',
       name: '月度信息采集2',
-      component:'./monthlyInfoCollection/index',
+      component: './monthlyInfoCollection/index',
     },
     {
-      path:'/departmentCostCheck',
+      path: '/departmentCostCheck',
       name: '科室损益查询',
-      component:'./departmentCostCheck/index',
+      routes: [
+        {
+          path: '/departmentCostCheck/month',
+          name: '月度损益查询',
+          component: './departmentCostCheck/index',
+        },
+        {
+          path: '/departmentCostCheck/year',
+          name: '年度损益查询',
+          component: './departmentCostCheck/index',
+        },
+      ],
+    },
+    {
+      path: '/hospitalProfitAndLoss',
+      name: '全院损益查询',
+      routes: [
+        {
+          path: '/hospitalProfitAndLoss/month',
+          name: '月度损益查询',
+          component: './hospitalProfitAndLoss/index',
+        },
+        {
+          path: '/hospitalProfitAndLoss/year',
+          name: '年度损益查询',
+          component: './hospitalProfitAndLoss/index',
+        },
+      ],
     },
     {
-      path:'/departmentMzCostCalc',
+      path: '/departmentMzCostCalc',
       name: '科室门住损益计算',
-      component:'./departmentMenzhuCostCalc/index',
+      component: './departmentMenzhuCostCalc/index',
     },
     {
-      path:'/specialDataImport',
+      path: '/specialDataImport',
       name: '填报数据导入',
-      component:'./specialDataImport/index',
+      component: './specialDataImport/index',
     },
     {
-      path:'/incomeCollectionAction',
+      path: '/incomeCollectionAction',
       name: '收入归集',
-      component:'./incomeCollectionAction/index',
+      component: './incomeCollectionAction/index',
     },
     {
       path: '/monthlyInfoSearch',
@@ -283,7 +320,7 @@ export default defineConfig({
             },
           ]
         }
-        
+
       ],
     },
     {

+ 5 - 2
package.json

@@ -1,5 +1,6 @@
 {
   "name": "CostAccountingSys",
+  "version": "1.0.0",
   "private": true,
   "author": "code4eat <awesomedema@gmail.com>",
   "scripts": {
@@ -27,8 +28,10 @@
     "react-ace": "^10.1.0",
     "react-monaco-editor": "^0.55.0",
     "react-sortable-hoc": "^2.0.0",
+    "react-window": "^2.1.2",
     "umi-request": "^1.4.0",
-    "xlsx": "^0.18.5"
+    "xlsx": "^0.18.5",
+    "xlsx-js-style": "1.2.0"
   },
   "devDependencies": {
     "@types/file-saver": "^2.0.5",
@@ -44,4 +47,4 @@
     "prettier-plugin-packagejson": "^2",
     "typescript": "^4.1.2"
   }
-}
+}

+ 25 - 18
src/app.tsx

@@ -8,12 +8,19 @@
  */
 // 运行时配置
 
+// 打印版本信息(在模块加载时立即执行)
+(function printVersion() {
+  console.log(
+    '%c 成本核算系统 %c v' + process.env.APP_VERSION + ' %c 构建时间:' + process.env.BUILD_TIME + ' ',
+    'background: #3377FF; color: #fff; padding: 2px 6px; border-radius: 3px 0 0 3px;',
+    'background: #35495e; color: #fff; padding: 2px 6px;',
+    'background: #41b883; color: #fff; padding: 2px 6px; border-radius: 0 3px 3px 0;'
+  );
+})();
+
 // 全局初始化数据配置,用于 Layout 用户信息和权限初始化
 // 更多信息见文档:https://next.umijs.org/docs/api/runtime-config#getinitialstate
 
-
-
-
 import { AxiosResponse, history } from '@umijs/max';
 import { message, notification, Modal, Menu } from 'antd';
 import type { RequestConfig } from 'umi';
@@ -273,7 +280,7 @@ export const request: RequestConfig = {
                 //console.log({code,'response.data':response.data});
                 return response.data
               } else {
-           
+
                 notification.error({
                   top: 72,
                   message: '提示',
@@ -282,7 +289,7 @@ export const request: RequestConfig = {
                   icon: <IconFont type="icon-jinggaotishi" />,
                   style: { padding: 16, borderRadius: 8 }
                 })
-               return false
+                return false
               }
             }
 
@@ -385,7 +392,7 @@ export function patchClientRoutes({ routes }: { routes: any }) {
         treeLoop(a);
       })
     }
-    
+
 
   }
 
@@ -411,19 +418,19 @@ export const layout = ({ initialState, setInitialState }: { initialState: any, s
   };
 
   const checkPermission = (path: string) => {
-    const { memuData = [] } = initialState || {};  
+    const { memuData = [] } = initialState || {};
     // 检查是否为无访问权限页面
     if (path === '/noAccess') {
       return true;
     }
-  
+
     // 检查访问权限
     const hasAccess = checkAccess([...memuData], path);
-  
+
     return hasAccess;
   };
-  
-  
+
+
 
 
   // useEffect(() => {
@@ -432,7 +439,7 @@ export const layout = ({ initialState, setInitialState }: { initialState: any, s
   //       const path = removePrefix(location.location.pathname, '/CostAccountingSys');
   //       const hasPermission = checkPermission(path);
   //       console.log({ hasPermission, path });
-  
+
   //       // 如果hasPermission为true,确保不会跳转到/noAccess
   //       if (!hasPermission && path !== '/noAccess') {
   //         history.push('/noAccess');
@@ -441,12 +448,12 @@ export const layout = ({ initialState, setInitialState }: { initialState: any, s
   //     isMenuClickRef.current = false; // 重置状态
   //   });
   // }, [initialState]);
-  
-  
+
+
 
 
   return {
-    
+
     menuHeaderRender: false,
     disableMobile: true,
     onPageChange: () => { },
@@ -465,7 +472,7 @@ export const layout = ({ initialState, setInitialState }: { initialState: any, s
     },
     collapsed: isCollapsed,
     // fixSiderbar:false,
-    menuItemRender:(item: { path: any; }, dom: string | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | ReactFragment | ReactPortal | null | undefined) => {
+    menuItemRender: (item: { path: any; }, dom: string | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | ReactFragment | ReactPortal | null | undefined) => {
       return (
         <a
           onClick={() => {
@@ -477,8 +484,8 @@ export const layout = ({ initialState, setInitialState }: { initialState: any, s
         </a>
       );
     },
-    menu:{
-      request:async ()=>{
+    menu: {
+      request: async () => {
         const userData = localStorage.getItem('userData');
         const currentSelectedTab = localStorage.getItem('currentSelectedTab');
         if (currentSelectedTab) {

+ 22 - 4
src/components/KCIMLeftList/index.tsx

@@ -2,7 +2,7 @@
  * @Author: code4eat awesomedema@gmail.com
  * @Date: 2024-03-19 10:27:43
  * @LastEditors: code4eat awesomedema@gmail.com
- * @LastEditTime: 2024-09-09 11:28:25
+ * @LastEditTime: 2025-08-29 16:53:45
  * @FilePath: /CostAccountingSys/src/components/KCIMLeftList/index.tsx
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */
@@ -45,7 +45,11 @@ export interface KCIMLeftListProps {
     contentH?: number|string,
     rowKey?: string,
     fieldNames?: { title?: string, key?: string, children?: string },
-    onChange?: (selectedItem: ListItem) => void
+    onChange?: (selectedItem: ListItem) => void,
+    onReachBottom?: () => void,
+    loading?: boolean,
+    useRemoteSearch?: boolean,
+    onSearchChange?: (keyword: string) => void
 }
 
 
@@ -103,7 +107,7 @@ const findFirstLeafNodeWithParents: any = (node: any, children: string) => {
 
 export const KCIMLeftList = (props: KCIMLeftListProps) => {
 
-    const { searchKey, placeholder = '请输入', onChange, listType = 'list', dataSource: data = [], icon, contentH = 500, rowKey = 'id', fieldNames = { title: 'title', key: 'key', children: 'children' } } = props;
+    const { searchKey, placeholder = '请输入', onChange, listType = 'list', dataSource: data = [], icon, contentH = 500, rowKey = 'id', fieldNames = { title: 'title', key: 'key', children: 'children' }, onReachBottom, loading = false, useRemoteSearch = false, onSearchChange } = props;
     
     const [dataSource, set_dataSource] = useState<ListItem[] | TreeNodeItem[]>([]);
     const [showList, set_showList] = useState<ListItem[] | TreeNodeItem[]>([]);
@@ -160,6 +164,16 @@ export const KCIMLeftList = (props: KCIMLeftListProps) => {
     }, [data]);
 
 
+    const handleScroll = (e: any) => {
+        const target = e.currentTarget;
+        if (!target) return;
+        const { scrollTop, scrollHeight, clientHeight } = target;
+        const nearBottom = scrollHeight - (scrollTop + clientHeight) <= 20;
+        if (nearBottom && onReachBottom && !loading) {
+            onReachBottom();
+        }
+    }
+
     return (
         <div className="KCIMLeftList">
             <div className='toolbar'>
@@ -169,6 +183,10 @@ export const KCIMLeftList = (props: KCIMLeftListProps) => {
                     }
                     // style={{ width: 156 }}
                     onChange={(e) => {
+                        if (useRemoteSearch) {
+                            onSearchChange && onSearchChange(e.target.value);
+                            return;
+                        }
                         if (listType == 'list') {
                             const result = dataSource.filter(item => item[`${searchKey}`].indexOf(e.target.value) != -1);
                             set_showList(result);
@@ -184,7 +202,7 @@ export const KCIMLeftList = (props: KCIMLeftListProps) => {
                 {/* <UpDataActBtn key={'act'} record={undefined} type='ADD_LEFTDATA' /> */}
 
             </div>
-            <div className='wrap' style={{ height: contentH, overflowY: 'scroll' }}>
+            <div className='wrap_cost' style={{ height: contentH, overflowY: 'scroll' }} onScroll={handleScroll}>
                 {
                     listType == 'list' && showList.map((item, index) => {
                         return (

+ 2 - 1
src/components/KCIMLeftList/style.less

@@ -4,6 +4,7 @@
 .KCIMLeftList {
     border-radius: 4px;
     padding:8px 0;
+    padding-top: 0;
     width: 100%;
     height:100%;
     overflow-y: hidden;
@@ -34,7 +35,7 @@
         }
       }
   
-      .wrap {
+      .wrap_cost {
         margin-top: 12px;
         .cost-ant-tree.cost-ant-tree-directory .cost-ant-tree-treenode-selected:hover::before,
         .cost-ant-tree.cost-ant-tree-directory .cost-ant-tree-treenode-selected::before {

+ 58 - 35
src/components/KCIMTable/style.less

@@ -10,9 +10,7 @@
         border-start-end-radius: 4px !important;
     }
 
-    .cost-ant-table-row-indent+.cost-ant-table-row-expand-icon {
-        //margin-top: -2px !important;
-    }
+    // 移除空规则,避免linter警告
 }
 
 .cost-ant-table .cost-ant-table-header {
@@ -52,41 +50,39 @@
 
         .cost-ant-table-thead {
             .cost-ant-table-cell {
-                border-bottom: none !important;
                 padding: 8px 8px !important;
-                //border-top:1px solid #dae2f2 !important ;
                 background: rgb(238 243 250 / 100%) !important;
-                //border-right: 1px solid  #dae2f2 !important;
-
-                a {
-                    color: #3376FE;
-                }
+                // 表头允许换行
+                white-space: normal !important;
+                word-break: break-word !important;
+                overflow-wrap: anywhere;
+                // 多层级表头边框
+                border-bottom: 1px solid #dae2f2 !important;
+                border-right: 1px solid #dae2f2 !important;
+            }
+            // 首列左边框
+            > tr > th:first-child { border-left: 1px solid #dae2f2 !important; }
 
+            a {
+                color: #3376FE;
+            }
 
-                .cost-ant-table-selection {
-                    .cost-ant-table-selection-extra {
-                        top: 5px;
-                    }
-                }
 
-                &::before {
-                    display: none;
-                }
-
-                &:hover {
-                    background: rgb(238 243 250 / 100%) !important;
+            .cost-ant-table-selection {
+                .cost-ant-table-selection-extra {
+                    top: 5px;
                 }
+            }
 
-                &:last-child {
-                    border-right: none;
-                }
+            &::before {
+                display: none;
             }
 
-            .cost-ant-table-cell-fix-right {
-                &:last-child {
-                    right: 0 !important;
-                }
+            &:hover {
+                background: rgb(238 243 250 / 100%) !important;
             }
+
+            // 不再去掉最后一个表头格子的右边框,保持分隔
         }
 
         .cost-ant-table-tbody {
@@ -125,11 +121,7 @@
                             line-height:unset;
                         }
     
-                        &.cost-ant-table-cell-with-append {
-                            .cost-ant-table-row-expand-icon {
-                                 
-                            }
-                        }
+                        // 移除空规则,避免linter警告
     
                         // &:hover {
                         //     border-radius: 0 !important;
@@ -158,8 +150,8 @@
 
         .cost-ant-table-summary {
             &>tr>td {
-                text-align: left !important;
-                padding: 12px 8px !important;
+                // 不强制对齐,让各列按照 Summary.Cell 的 align 与列配置对齐
+                padding: 8px 8px !important; // 与tbody行保持一致高度
                 border-top: 1px solid #dae2f2 !important;
             }
         }
@@ -234,4 +226,35 @@
 .cost-ant-empty-normal {
     margin-block: 80px !important;
     color: rgb(0 0 0 / 25%);
+}
+
+// 小计行高亮背景样式
+.subtotal-row {
+    background: #F2F8FF !important;
+    font-weight: 600; // 小计行加粗
+}
+
+// 兼容固定列:为固定列的单元格也着色
+.KCIMTable {
+    .cost-ant-table {
+        .cost-ant-table-tbody {
+            tr.subtotal-row > td,
+            tr.subtotal-row > td.cost-ant-table-cell-fix-left,
+            tr.subtotal-row > td.cost-ant-table-cell-fix-right,
+            tr.subtotal-row > td.cost-ant-table-cell-fix-left-last,
+            tr.subtotal-row > td.cost-ant-table-cell-fix-right-first {
+                background: #F2F8FF !important;
+                position: relative;
+                font-weight: 600; // 与整行一致加粗
+            }
+            // 去掉固定列的阴影遮罩,避免覆盖行背景
+            tr.subtotal-row > td.cost-ant-table-cell-fix-left::after,
+            tr.subtotal-row > td.cost-ant-table-cell-fix-left-last::after,
+            tr.subtotal-row > td.cost-ant-table-cell-fix-right::after,
+            tr.subtotal-row > td.cost-ant-table-cell-fix-right-first::after {
+                box-shadow: none !important;
+                background: transparent !important;
+            }
+        }
+    }
 }

+ 25 - 13
src/global.less

@@ -3,13 +3,14 @@
 }
 
 /* Safari */
-@media screen and (-webkit-min-device-pixel-ratio:0)
+@media screen and (min-device-pixel-ratio:0)
 {
   * {
     margin: 0;
     padding: 0;
     font-family:system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
   }
+
   #password {
     &::placeholder {
       font-family:system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
@@ -28,7 +29,7 @@ body {
 input:-webkit-autofill,
 textarea:-webkit-autofill,
 select:-webkit-autofill {
-  -webkit-box-shadow: 0 0 0 1000px white inset !important;
+  box-shadow: 0 0 0 1000px white inset !important;
   box-shadow: 0 0 0 1000px white inset !important;
   -webkit-text-fill-color: #000 !important;
 }
@@ -38,7 +39,7 @@ select:-webkit-autofill {
 
 .cost-ant-space-item {
     &>a {
-        color: #3377FF !important;
+        color: #37F !important;
     }
 }
 
@@ -50,8 +51,9 @@ textarea {
 }
 
 .cost-ant-btn-primary {
-    background: #3377FF;
+    background: #37F;
 }
+
 .cost-ant-btn-primary[disabled]{
     background:#f5f5f5 ;
 }
@@ -59,8 +61,8 @@ textarea {
 .cost-ant-checkbox {
     &.cost-ant-checkbox-checked {
           .cost-ant-checkbox-inner {
-            background-color:#3377FF;
-            background: #3377FF;
+            background-color:#37F;
+            background: #37F;
           }
     }
     // &.cost-ant-checkbox-indeterminate {
@@ -73,7 +75,7 @@ textarea {
 }
 
 .cost-ant-checkbox-indeterminate .cost-ant-checkbox-inner::after {
-    background-color:#3377FF;
+    background-color:#37F;
 }
 
 .cost-ant-input-affix-wrapper {
@@ -109,11 +111,13 @@ textarea {
     .cost-ant-modal-content {
         padding: 16px !important;
         border-radius: 4px !important;
+
         .cost-ant-modal-header {
              padding: 0 ;
              margin-bottom: 16px;
              border-bottom: none;
         }
+
         .cost-ant-modal-body {
             max-height: 570px;
             overflow-y: scroll;
@@ -128,6 +132,7 @@ textarea {
                         height:18px;
                       
                     }
+
                     .cost-ant-modal-confirm-content {
                         max-width: 100% !important;
                         max-height: 498px;
@@ -194,7 +199,6 @@ textarea {
                                 .cost-ant-table-container {
                                     .cost-ant-table-content {
                                         .cost-ant-table-thead {
-
                                             &>tr>th {
                                                 font-size: 14px;
                                                 font-family: SourceHanSansCN-Medium, SourceHanSansCN;
@@ -248,10 +252,12 @@ textarea {
                 }
             }
         }
+
         .cost-ant-modal-footer {
             border-top: none;
             padding: 0;
             margin-top: 16px;
+
             .cost-ant-btn {
                  border-radius: 4px !important;
                  height: 24px !important;
@@ -342,7 +348,7 @@ textarea {
 // }
 
 .cost-ant-input-affix-wrapper {
-    padding: 0px 8px !important;
+    padding: 0 8px !important;
     border-radius: 4px;
     border: 1px solid #CFD7E6 !important;
 
@@ -419,6 +425,7 @@ textarea {
         color: #3376FE !important; 
     }
 }
+
 .cost-ant-menu-item-selected {
     color: #3376FE !important; 
     background-color:#F2F6FF !important;
@@ -427,6 +434,7 @@ textarea {
          color: #3376FE !important; 
     }
 }
+
 .cost-ant-menu-light:not(.cost-ant-menu-horizontal) .cost-ant-menu-item:not(.cost-ant-menu-item-selected):hover {
     background-color: #f0f2f5 !important;
     // margin:8px !important;
@@ -453,7 +461,6 @@ textarea {
             .cost-ant-table-content {
                 .cost-ant-table-thead {
                     &>tr>th {
-
                         height: 15px;
                         line-height: 15px;
                         font-size: 14px;
@@ -474,11 +481,10 @@ textarea {
                 .cost-ant-table-tbody {
                     .cost-ant-table-row {
                         .cost-ant-table-cell {
-
                             height: 15px;
                             line-height: 15px;
                             font-size: 14px;
-                            padding: 8px 8px !important;
+                            padding: 8px !important;
                             font-family: SourceHanSansCN-Medium, SourceHanSansCN;
                             color: #17181A;
 
@@ -617,6 +623,7 @@ textarea {
         padding: 5px 0 !important;
         padding-top: 12px !important;
     }
+
     .cost-ant-tabs-nav {
         &::before {
             border-bottom: none !important;
@@ -699,6 +706,7 @@ textarea {
 
 .cost-ant-pro .cost-ant-pro-layout .cost-ant-pro-sider .cost-ant-layout-sider-children {
     padding-inline: 0 !important;
+
     &>div {
        &>ul {
           padding-inline: 8px !important;
@@ -729,6 +737,7 @@ textarea {
 
 .cost-ant-input-number-input {
     border-radius: 4px;
+
     &::placeholder {
         color: #99A6BF !important;
     }
@@ -756,6 +765,7 @@ textarea {
     // color: #99A6BF !important;
     border-radius: 4px;
     box-shadow: none !important;
+
     .cost-ant-input-number-input {
         height: 22px !important;
         line-height: 22px !important;
@@ -779,12 +789,14 @@ textarea {
     border-radius: 4px !important;
     border: 1px solid #CFD7E6 !important;
     height: 24px;
+
     .cost-ant-picker-input {
         &>input {
              &::placeholder {
                 color:#99A6BF !important;
              }
         }
+
         .cost-ant-picker-suffix {
             color:#99A6BF !important;
         }
@@ -800,7 +812,7 @@ textarea {
                         td {
                             &.cost-ant-picker-cell-selected {
                                 .cost-ant-picker-cell-inner {
-                                    background:#3377FF;
+                                    background:#37F;
                                 }
                             }
                         }

+ 37 - 14
src/pages/baseSetting/otherItemSet/departmentCostCalc/index.tsx

@@ -2,7 +2,7 @@
  * @Author: code4eat awesomedema@gmail.com
  * @Date: 2023-03-03 11:30:33
  * @LastEditors: code4eat awesomedema@gmail.com
- * @LastEditTime: 2025-05-13 11:12:48
+ * @LastEditTime: 2025-10-30 10:24:16
  * @FilePath: /KC-MiddlePlatform/src/pages/platform/setting/pubDicTypeMana/index.tsx
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */
@@ -20,7 +20,7 @@ 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 * as XLSX from 'xlsx';
+import * as XLSX from 'xlsx-js-style';
 import { saveAs } from 'file-saver';
 import moment from 'moment';
 import 'moment/locale/zh-cn';
@@ -597,18 +597,23 @@ export default function DepartmentCostCalc() {
 
 
     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);
-            headerRows[level].push({ title: col.title, colSpan, rowSpan: col.children ? 1 : maxLevel - level });
             if (col.children) {
+                // 父节点仅进行横向合并,不做纵向合并
+                headerRows[level].push({ title: col.title, colSpan, rowSpan: 1 });
                 getHeaderRows(col.children, level + 1, headerRows, maxLevel);
             } else {
-                // 填充空白单元格
-                for (let i = level + 1; i < maxLevel; i++) {
+                // 叶子:从当前层到倒数第二层使用空白占位,叶子标题写入最底层
+                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 });
             }
         });
         return headerRows;
@@ -726,17 +731,34 @@ export default function DepartmentCostCalc() {
                 }
             });
 
-            XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
+            // 表头加粗:对 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;
+                });
+            });
 
-            const wbout = XLSX.write(workbook, { bookType: 'xlsx', type: 'binary' });
-            const s2ab = (s: string) => {
-                const buf = new ArrayBuffer(s.length);
-                const view = new Uint8Array(buf);
-                for (let i = 0; i < s.length; i++) view[i] = s.charCodeAt(i) & 0xFF;
-                return buf;
-            };
+            XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
 
-            saveAs(new Blob([s2ab(wbout)], { type: 'application/octet-stream' }), currentTab ? `${currentTab.label}.xlsx` : 'table_data.xlsx');
+            // 使用库自带的 writeFile 以最大化样式兼容性
+            XLSX.writeFile(workbook, currentTab ? `${currentTab.label}.xlsx` : 'table_data.xlsx');
         } catch (error) {
             console.error('Export failed:', error);
         }
@@ -902,3 +924,4 @@ export default function DepartmentCostCalc() {
         </KCIMPagecontainer>
     )
 }
+    

+ 127 - 0
src/pages/costAccounting/calcPageTemplate/FilterBar.tsx

@@ -0,0 +1,127 @@
+import React from 'react';
+import moment from 'moment';
+import 'moment/locale/zh-cn';
+import { DatePicker, Input } from 'antd';
+import locale from 'antd/es/date-picker/locale/zh_CN';
+import { ProFormCascader, ProFormSelect } from '@ant-design/pro-components';
+import { SearchOutlined } from '@ant-design/icons';
+
+interface FilterItemConfig {
+  type: 'input' | 'select' | 'cascader';
+  key: string;
+  label: string;
+  placeholder?: string;
+  request?: () => Promise<any[]>;
+}
+
+interface FilterBarProps {
+  computeDate: string;
+  onDateChange: (dateString: string) => void;
+  filterConf: FilterItemConfig[];
+  inputValues: Record<string, any>;
+  onInputChange: (key: string, value: string) => void;
+  onInputSearch: (key: string, value?: string) => void;
+  onSelectChange: (key: string, value: any) => void;
+  onCascaderChange: (key: string, value: any[] | null) => void;
+}
+
+/**
+ * 筛选栏组件:
+ * - 受控的核算年月选择器
+ * - 根据 filterConf 动态渲染 input/select/cascader
+ */
+export default function FilterBar(props: FilterBarProps) {
+  const { computeDate, onDateChange, filterConf, inputValues, onInputChange, onInputSearch, onSelectChange, onCascaderChange } = props;
+
+  return (
+    <>
+      <div className="filterItem">
+        <div className="search">
+          <span>核算年月:</span>
+          <DatePicker
+            onChange={(data, dateString) => onDateChange(dateString)}
+            picker="month"
+            locale={locale}
+            value={computeDate ? moment(computeDate, 'YYYY-MM') : undefined}
+            format="YYYY-MM"
+            autoComplete='off'
+            placeholder="选择年月"
+          />
+        </div>
+      </div>
+      {filterConf.map((item) => {
+        const { type, key, label, placeholder, request } = item as any;
+        if (type === 'input') {
+          return (
+            <div className='filterItem' style={{ marginLeft: 16, width: 205 }} key={key}>
+              <span className='label' style={{ whiteSpace: 'nowrap' }}>{label}</span>
+              <Input
+                placeholder={placeholder || ''}
+                allowClear
+                autoComplete='off'
+                suffix={<SearchOutlined style={{ color: '#99A6BF' }} onClick={() => onInputSearch(key, inputValues[key] || '')} />}
+                value={inputValues[key] || ''}
+                onChange={(e) => {
+                  const value = e.target.value;
+                  onInputChange(key, value);
+                  if (value.length === 0) {
+                    onInputSearch(key, '');
+                  }
+                }}
+                onPressEnter={(e) => onInputSearch(key, (e.target as HTMLInputElement).value)}
+              />
+            </div>
+          );
+        }
+        if (type === 'select') {
+          return (
+            <div className='filterItem' style={{ marginLeft: 16 }} key={key}>
+              <span className='label'>{label}</span>
+              <ProFormSelect
+                noStyle
+                allowClear
+                placeholder="请选择"
+                style={{ width: 160, marginRight: 16 }}
+                request={async () => {
+                  const arr = request ? await request() : [];
+                  return arr || [];
+                }}
+                fieldProps={{
+                  onChange(value) {
+                    onSelectChange(key, value);
+                  },
+                }}
+              />
+            </div>
+          );
+        }
+        if (type === 'cascader') {
+          return (
+            <div className='filterItem' style={{ marginLeft: 16 }} key={key}>
+              <span className='label'>{label}</span>
+              <ProFormCascader
+                noStyle
+                allowClear
+                placeholder="请选择"
+                request={async () => {
+                  const resp = request ? await request() : [];
+                  return resp || [];
+                }}
+                fieldProps={{
+                  showSearch: true,
+                  fieldNames: { label: 'responsibilityName', value: 'responsibilityCode', children: 'child' },
+                  onChange(value: any) {
+                    onCascaderChange(key, value);
+                  },
+                }}
+              />
+            </div>
+          );
+        }
+        return <React.Fragment key={key} />
+      })}
+    </>
+  );
+}
+
+

+ 25 - 31
src/pages/costAccounting/calcPageTemplate/columns.tsx

@@ -1241,14 +1241,14 @@ export const hospitalDeptVisitCost: ProColumns[] = [
         width: 180,
         fixed: 'left' as any,
     },
+    {
+        title: '服务量',
+        dataIndex: 'serviceCount',
+        align: 'right',
+    },
     {
         title: '每诊次成本',
         children: [
-            {
-                title: '服务量',
-                dataIndex: 'serviceCount',
-                align: 'right',
-            },
             {
                 title: '医疗成本',
                 dataIndex: 'medicalCost',
@@ -1329,10 +1329,7 @@ export const hospitalServiceProjectCost: ProColumns[] = [
         dataIndex: 'itemName',
         width: 260,
         fixed: 'left' as any,
-        renderText: (_: any, record: any) => {
-            const { itemCode, itemName } = record || {};
-            return itemCode ? `[${itemCode}]${itemName || ''}` : (itemName || '');
-        },
+        renderText: (_: any, record: any) => (record?.itemName ?? ''),
     },
     {
         title: '医疗成本',
@@ -1432,17 +1429,14 @@ export const diseaseCostCompositionDetail: ProColumns[] = [
     },
     {
         title: '病种成本',
-        children: [
-            {
-                title: '金额',
-                dataIndex: 'totalCost',
-                align: 'right',
-                renderText(num: any, record: any) {
-                    const { decimalPlace, permil } = record || {};
-                    return formatMoneyNumber(num, { decimalPlaces: (typeof decimalPlace === 'number') ? decimalPlace : 2, useThousandSeparator: permil === 1 || permil === true || true });
-                },
-            },
-        ] as any,
+        dataIndex: 'totalCost',
+        width: 150,
+        fixed: 'left' as any,
+        align: 'right',
+        renderText(num: any, record: any) {
+            const { decimalPlace, permil } = record || {};
+            return formatMoneyNumber(num, { decimalPlaces: (typeof decimalPlace === 'number') ? decimalPlace : 2, useThousandSeparator: permil === 1 || permil === true || true });
+        },
     },
     {
         title: '人员经费',
@@ -1707,7 +1701,7 @@ export const drgCostCompositionDetail: ProColumns[] = [
         fixed: 'left' as any,
     },
     {
-        title: '病种成本',
+        title: 'DRG成本',
         dataIndex: 'totalCost',
         align: 'right',
         renderText(num: any, record: any) {
@@ -1945,32 +1939,32 @@ export const clinicalDeptMedicalCost: ProColumns[] = [
     {
         title: '固定资产折旧费(4)',
         children: [
-            { title: '直接成本', dataIndex: 'fixedAssetDepreciationDirect', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
-            { title: '间接成本', dataIndex: 'fixedAssetDepreciationIndirect', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
-            { title: '全成本', dataIndex: 'fixedAssetDepreciationTotal', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
+            { title: '直接成本', dataIndex: 'fixedAssetDepreciationDirectCost', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
+            { title: '间接成本', dataIndex: 'fixedAssetDepreciationIndirectCost', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
+            { title: '全成本', dataIndex: 'fixedAssetDepreciationTotalCost', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
         ] as any,
     },
     {
         title: '无形资产摊销费(5)',
         children: [
-            { title: '直接成本', dataIndex: 'intangibleAssetAmortizationDirect', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
-            { title: '间接成本', dataIndex: 'intangibleAssetAmortizationIndirect', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
-            { title: '全成本', dataIndex: 'intangibleAssetAmortizationTotal', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
+            { title: '直接成本', dataIndex: 'intangibleAssetAmortizationDirectCost', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
+            { title: '间接成本', dataIndex: 'intangibleAssetAmortizationIndirectCost', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
+            { title: '全成本', dataIndex: 'intangibleAssetAmortizationTotalCost', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
         ] as any,
     },
     {
         title: '提取医疗风险基金(6)',
         children: [
-            { title: '直接成本', dataIndex: 'medicalRiskReserveDirect', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
-            { title: '间接成本', dataIndex: 'medicalRiskReserveIndirect', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
+            { title: '直接成本', dataIndex: 'medicalRiskReserveDirectCost', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
+            { title: '间接成本', dataIndex: 'medicalRiskReserveIndirectCost', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
             { title: '全成本', dataIndex: 'medicalRiskReserveTotalCost', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
         ] as any,
     },
     {
         title: '其他医疗费用(7)',
         children: [
-            { title: '直接成本', dataIndex: 'otherMedicalExpensesDirect', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
-            { title: '间接成本', dataIndex: 'otherMedicalExpensesIndirect', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
+            { title: '直接成本', dataIndex: 'otherMedicalExpensesDirectCost', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
+            { title: '间接成本', dataIndex: 'otherMedicalExpensesIndirectCost', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
             { title: '全成本', dataIndex: 'otherMedicalExpensesTotalCost', align: 'right', renderText: (n: string | number | null | undefined, r: { decimalPlace: number | undefined; }) => formatMoneyNumber(n, { decimalPlaces: (typeof r?.decimalPlace === 'number') ? r.decimalPlace : 2, useThousandSeparator: true }) },
         ] as any,
     },

+ 59 - 0
src/pages/costAccounting/calcPageTemplate/config.ts

@@ -25,6 +25,46 @@ export const unitPersonnelCostCalcFilterConf = [
     }
 ]
 
+// 医院科室直接成本表(医疗成本)- 筛选配置
+export const deptDirectMedicalCostFilterConf = [
+    {
+        type:'input',
+        label:'科室名称:',
+        placeholder:'请输入',
+        key:'departmentName'
+    }
+]
+
+// 医院科室直接成本表(全成本)- 筛选配置(前端检索)
+export const deptFullDirectCostFilterConf = [
+    {
+        type:'input',
+        label:'科室名称:',
+        placeholder:'请输入',
+        key:'departmentName'
+    }
+]
+
+// 临床服务类科室全成本(医疗成本)- 筛选(前端检索)
+export const clinicalDeptMedicalCostFilterConf = [
+    {
+        type:'input',
+        label:'科室名称:',
+        placeholder:'请输入',
+        key:'departmentName'
+    }
+]
+
+// 临床服务类科室全成本(全成本)- 筛选(前端检索)
+export const clinicalDeptFullCostFilterConf = [
+    {
+        type:'input',
+        label:'科室名称:',
+        placeholder:'请输入',
+        key:'departmentName'
+    }
+]
+
 export const patientCostCalcFilterConf = [
     {
         type:'input',
@@ -307,6 +347,15 @@ export const projectCostCalcFilterConf = [
         key:'itemFilter'
     }
 ];
+// 医院医疗服务项目成本明细表 - 项目编码/名称检索
+export const medicalServiceCostDetailFilterConf = [
+    {
+        type:'input',
+        label:'检索:',
+        placeholder:'项目代码/名称',
+        key:'itemFilter'
+    }
+];
 
 export const PatientItemCalcFilterConf = [
     {
@@ -404,5 +453,15 @@ export const standItemShareCostCalcFilterConf = [
     }
 ];
 
+// 医院医疗服务项目成本汇总表 - 项目名称检索(本地检索)
+export const hospitalServiceProjectCostFilterConf = [
+    {
+        type:'input',
+        label:'项目名称:',
+        placeholder:'请输入',
+        key:'itemName'
+    }
+];
+
 
 

+ 251 - 154
src/pages/costAccounting/calcPageTemplate/index.tsx

@@ -95,6 +95,7 @@ export default function calcPageTemplate() {
   const [filterConf, set_filterConf] = useState<any[]>([]);
   const [scrollX, set_scrollX] = useState(500);
   const [ifShowCalcBtn, set_ifShowCalcBtn] = useState(true);
+  const [ifShowExportBtn, set_ifShowExportBtn] = useState(false);
   const [totalCount, set_totalCount] = useState<undefined | number>(undefined);
   const [inputValues, setInputValues] = useState<{ [key: string]: any }>({});
   const [pagePermissions, set_pagePermissions] = useState<Set<string>>(new Set());
@@ -108,6 +109,7 @@ export default function calcPageTemplate() {
   const [isModalVisible, setIsModalVisible] = useState(false);
   const [openProcessModal, set_openProcessModal] = useState(false);
   const [allParentsKeys, set_allParentsKeys] = useState<any[]>([]);
+  const [hospitalServiceProjectExpandedKeys, set_hospitalServiceProjectExpandedKeys] = useState<any[]>([]);
   const [dataSource, set_dataSource] = useState<any[]>([]);
   const exporter = useRef(createPagedExporter<{ [k: string]: any }>({
     pageSize: 100,
@@ -155,26 +157,32 @@ export default function calcPageTemplate() {
       const { departmentName, ...rest } = reqParams;
       reqParams = rest;
     }
-    if (params.calcPageKey === 'deptCostAllocationSummary' && 'departmentName' in reqParams) {
-      // deptCostAllocationSummary 仅前端检索
+    if (params.calcPageKey === 'hospitalDeptVisitCost' && 'departmentName' in reqParams) {
+      // hospitalDeptVisitCost 仅前端检索
       const { departmentName, ...rest } = reqParams;
       reqParams = rest;
     }
-    if (params.calcPageKey === 'diseaseCostDetail' && 'itemName' in reqParams) {
-      // diseaseCostDetail 病种名称仅前端检索:不将 itemName 传给后端
-      const { itemName, ...rest } = reqParams;
+    if (params.calcPageKey === 'deptCostAllocationSummary' && 'departmentName' in reqParams) {
+      // deptCostAllocationSummary 仅前端检索
+      const { departmentName, ...rest } = reqParams;
       reqParams = rest;
     }
-    if (params.calcPageKey === 'diseaseCostCompositionDetail' && 'itemName' in reqParams) {
-      // diseaseCostCompositionDetail 病种名称仅前端检索
-      const { itemName, ...rest } = reqParams;
+    if (params.calcPageKey === 'hospitalDeptBedDayCost' && 'departmentName' in reqParams) {
+      // hospitalDeptBedDayCost 仅前端检索
+      const { departmentName, ...rest } = reqParams;
       reqParams = rest;
     }
+    // diseaseCostDetail 和 diseaseCostCompositionDetail 已改为后端分页+检索,diseaseFilter 参数传给后端
     if (params.calcPageKey === 'deptDiseaseCostCompositionDetail' && 'itemName' in reqParams) {
       // 服务单元病种成本构成明细表:病种名称仅前端检索
       const { itemName, ...rest } = reqParams;
       reqParams = rest;
     }
+    if (params.calcPageKey === 'medicalServiceCostDetail' && 'itemFilter' in reqParams) {
+      // medicalServiceCostDetail 仅前端检索:项目代码/名称
+      const { itemFilter, ...rest } = reqParams as any;
+      reqParams = rest;
+    }
 
     const resp = await getCalcPageTableData({ ...reqParams });
     if (resp) {
@@ -317,9 +325,9 @@ export default function calcPageTemplate() {
             totalPage: 1,
           };
         }
-        // 新增:服务单元病种成本构成明细表(动态多级表头+数据)
+        // 新增:服务单元病种成本构成明细表(动态多级表头+数据,后端分页+检索
         if (params.calcPageKey === 'deptDiseaseCostCompositionDetail') {
-          const { title = [], data: rowList = [] } = resp || {};
+          const { title = [], data: rowList = [], totalCount, pageSize, totalPage } = resp || {};
 
           // 递归生成多级列(透传 dataType/decimalPlace/isFixed/permil,并挂载统一 renderText 与 __meta)
           const buildColumns = (nodes: any[]): ProColumns[] => {
@@ -359,7 +367,7 @@ export default function calcPageTemplate() {
           const dynamicCols: ProColumns[] = [
             { title: '病种编码', dataIndex: 'reportCode', key: 'reportCode', width: 140, fixed: 'left' as any },
             { title: '病种名称', dataIndex: 'reportName', key: 'reportName', width: 220, fixed: 'left' as any },
-            { title: '病种成本', dataIndex: 'totalValue', key: 'totalValue', width: 150, align: 'right', renderText: (num: any) => formatMoneyNumber(num, { decimalPlaces: 2, useThousandSeparator: true }) } as any,
+            { title: '病种成本', dataIndex: 'totalValue', key: 'totalValue', width: 150, align: 'right', /* 该列也 fixed 左侧,三列一起固定 */ fixed: 'left' as any, renderText: (num: any) => formatMoneyNumber(num, { decimalPlaces: 2, useThousandSeparator: true }) } as any,
             ...buildColumns(title)
           ];
           set_columns(dynamicCols);
@@ -382,7 +390,8 @@ export default function calcPageTemplate() {
           const dynamicScrollX = sumLeafWidths(dynamicCols.slice(3) as any[]) + baseWidth + 80;
           set_scrollX(dynamicScrollX);
 
-          const data = (rowList || []).map((row: any, idx: number) => {
+          const rows = Array.isArray(rowList) ? rowList : [];
+          const data = rows.map((row: any, idx: number) => {
             const rowData: any = { reportCode: row.reportCode, reportName: row.reportName, totalValue: row.totalValue };
             ((row?.data || []) as any[]).forEach((cell: any) => {
               rowData[String(cell.code)] = cell.value;
@@ -393,15 +402,24 @@ export default function calcPageTemplate() {
           return {
             data,
             success: true,
-            total: Array.isArray(data) ? data.length : 0,
-            pageSize: Array.isArray(data) ? data.length : 0,
-            totalPage: 1,
+            total: (typeof totalCount === 'number' ? totalCount : (Array.isArray(data) ? data.length : 0)),
+            pageSize: (typeof pageSize === 'number' ? pageSize : (Array.isArray(data) ? data.length : 0)),
+            totalPage: (typeof totalPage === 'number' ? totalPage : 1),
           };
         }
         // 新增:医院服务单元DRG成本构成明细表(动态多级表头+数据)
         if (params.calcPageKey === 'deptDrgCostCompositionDetail') {
           const { title = [], data: rowList = [] } = resp || {};
 
+          // 本地检索:按 DRG 名称(reportName)过滤
+          let filteredRows = Array.isArray(rowList) ? rowList : [];
+          try {
+            const kw = String((params as any).itemName || '').trim();
+            if (kw) {
+              filteredRows = filteredRows.filter((row: any) => String(row?.reportName || '').includes(kw));
+            }
+          } catch { }
+
           const buildColumns = (nodes: any[]): ProColumns[] => {
             return (nodes || []).map((node: any) => {
               const hasChildren = Array.isArray(node.child) && node.child.length > 0;
@@ -438,7 +456,7 @@ export default function calcPageTemplate() {
           const dynamicCols: ProColumns[] = [
             { title: 'DRG编码', dataIndex: 'reportCode', key: 'reportCode', width: 140, fixed: 'left' as any },
             { title: 'DRG名称', dataIndex: 'reportName', key: 'reportName', width: 320, fixed: 'left' as any },
-            { title: '病种成本', dataIndex: 'totalValue', key: 'totalValue', width: 150, align: 'right', renderText: (num: any) => formatMoneyNumber(num, { decimalPlaces: 2, useThousandSeparator: true }) } as any,
+            { title: 'DRG成本', dataIndex: 'totalValue', key: 'totalValue', width: 150, align: 'right', renderText: (num: any) => formatMoneyNumber(num, { decimalPlaces: 2, useThousandSeparator: true }) } as any,
             ...buildColumns(title)
           ];
           set_columns(dynamicCols);
@@ -461,7 +479,7 @@ export default function calcPageTemplate() {
           const dynamicScrollX = sumLeafWidths(dynamicCols.slice(3) as any[]) + baseWidth + 80;
           set_scrollX(dynamicScrollX);
 
-          const data = (rowList || []).map((row: any, idx: number) => {
+          const data = (filteredRows || []).map((row: any, idx: number) => {
             const rowData: any = { reportCode: row.reportCode, reportName: row.reportName, totalValue: row.totalValue };
             ((row?.data || []) as any[]).forEach((cell: any) => {
               rowData[String(cell.code)] = cell.value;
@@ -494,7 +512,7 @@ export default function calcPageTemplate() {
         if (params.calcPageKey === 'deptDirectMedicalCost' || params.calcPageKey === 'deptCostAllocationSummary') {
           const totalIdx = Array.isArray(data) ? data.findIndex((row: any) => String(row?.responsibilityName || '').includes('总计')) : -1;
           if (totalIdx > -1) {
-            try { set_bottomSummaryRow(data[totalIdx]); } catch {}
+            try { set_bottomSummaryRow(data[totalIdx]); } catch { }
             data = [...data.slice(0, totalIdx), ...data.slice(totalIdx + 1)];
           } else {
             set_bottomSummaryRow(null);
@@ -542,13 +560,19 @@ export default function calcPageTemplate() {
             data = data.filter((row: any) => String(row.responsibilityName || '').includes(kw));
           }
         }
-        // 前端检索:diseaseCostDetail 按病种名称本地过滤
-        if (params.calcPageKey === 'diseaseCostDetail' && params.itemName) {
-          const kw = String(params.itemName || '').trim();
+        if (params.calcPageKey === 'hospitalDeptVisitCost' && params.departmentName) {
+          const kw = String(params.departmentName || '').trim();
           if (kw) {
-            data = data.filter((row: any) => String(row.itemName || '').includes(kw));
+            data = data.filter((row: any) => String(row.responsibilityName || '').includes(kw));
+          }
+        }
+        if (params.calcPageKey === 'hospitalDeptBedDayCost' && params.departmentName) {
+          const kw = String(params.departmentName || '').trim();
+          if (kw) {
+            data = data.filter((row: any) => String(row.responsibilityName || '').includes(kw));
           }
         }
+        // diseaseCostDetail 和 diseaseCostCompositionDetail 已改为后端检索,不再前端过滤
         // 前端检索:drgCostDetail / drgCostCompositionDetail / deptDrgCostCompositionDetail 按 DRG 名称本地过滤
         if ((params.calcPageKey === 'drgCostDetail' || params.calcPageKey === 'drgCostCompositionDetail' || params.calcPageKey === 'deptDrgCostCompositionDetail') && params.itemName) {
           const kw = String(params.itemName || '').trim();
@@ -563,11 +587,64 @@ export default function calcPageTemplate() {
             data = data.filter((row: any) => String(row.reportName || '').includes(kw));
           }
         }
-        // 前端检索:diseaseCostCompositionDetail 按病种名称本地过滤
-        if (params.calcPageKey === 'diseaseCostCompositionDetail' && params.itemName) {
+        // 前端检索:hospitalServiceProjectCost 按项目名称本地过滤(树结构兼容)
+        if (params.calcPageKey === 'hospitalServiceProjectCost' && params.itemName) {
           const kw = String(params.itemName || '').trim();
           if (kw) {
-            data = data.filter((row: any) => String(row.itemName || '').includes(kw));
+            const filterTreeByItemName = (nodes: any[]): any[] => {
+              if (!Array.isArray(nodes)) return [];
+              const results: any[] = [];
+              for (const node of nodes) {
+                const name = String(node?.itemName || '');
+                const rawChildren = (Array.isArray(node?.children) ? node.children : (Array.isArray(node?.child) ? node.child : [])) as any[];
+                const filteredChildren = filterTreeByItemName(rawChildren);
+                const selfMatch = name.includes(kw);
+                if (selfMatch || filteredChildren.length > 0) {
+                  const copy: any = { ...node };
+                  if (Array.isArray(node?.children)) copy.children = filteredChildren;
+                  else if (Array.isArray(node?.child)) copy.child = filteredChildren;
+                  else if (filteredChildren.length > 0) copy.children = filteredChildren;
+                  results.push(copy);
+                }
+              }
+              return results;
+            };
+            data = filterTreeByItemName(data);
+          }
+        }
+
+        // 规范化 hospitalServiceProjectCost 的树数据:统一 children,并生成稳定唯一的 id,避免展开联动
+        if (params.calcPageKey === 'hospitalServiceProjectCost') {
+          const normalizeTree = (nodes: any[], parentKey: string = ''): any[] => {
+            if (!Array.isArray(nodes)) return [];
+            return nodes.map((node: any, idx: number) => {
+              const copy: any = { ...node };
+              const rawChildren = Array.isArray(copy.children)
+                ? copy.children
+                : (Array.isArray(copy.child) ? copy.child : []);
+              const id = `${parentKey}${parentKey ? '-' : ''}${idx}`;
+              copy.id = id;
+              if (rawChildren && rawChildren.length > 0) {
+                copy.children = normalizeTree(rawChildren, id);
+              } else {
+                // 无子节点时去除空 children,避免渲染干扰
+                if (copy.children && Array.isArray(copy.children) && copy.children.length === 0) {
+                  delete copy.children;
+                }
+              }
+              // 统一去掉 child
+              if (copy.child) delete copy.child;
+              return copy;
+            });
+          };
+          data = normalizeTree(data);
+        }
+        // 前端检索:medicalServiceCostDetail 按项目代码或名称本地过滤
+        if (params.calcPageKey === 'medicalServiceCostDetail' && params.itemFilter) {
+          const kw = String(params.itemFilter || '').trim();
+          if (kw) {
+            const includesKw = (v: any) => String(v || '').includes(kw);
+            data = data.filter((row: any) => includesKw(row.itemCode) || includesKw(row.itemName));
           }
         }
 
@@ -625,7 +702,7 @@ export default function calcPageTemplate() {
           return false;
         }
         if (params.calcPageKey == 'wholeHospCostCalculate') {
-          const resp = await calculateReq(computeDate,params.calcPageKey, currentTabKey);
+          const resp = await calculateReq(computeDate, params.calcPageKey, currentTabKey);
           if (resp) {
             set_loading(false);
             message.success('操作成功!');
@@ -887,23 +964,27 @@ export default function calcPageTemplate() {
 
     // 优先尝试通过策略注册表处理(最小接入,不影响原有分支作为兜底)
     const strategy = getCalcPageStrategy(calcPageKey);
-    // 计算页面权限集合
+    // 计算页面权限集合(同步计算,直接使用 perms 避免 state 异步问题)
+    let perms = new Set<string>();
     try {
-      const perms = getPagePermissions(access, location.pathname.replace('/CostAccountingSys', ''));
+      perms = getPagePermissions(access, location.pathname.replace('/CostAccountingSys', ''));
       set_pagePermissions(perms);
-    } catch {}
+    } catch { }
     if (strategy) {
       const baseColumns = strategy.buildColumns();
       const enhanced = strategy.enhanceColumns ? strategy.enhanceColumns(baseColumns as any, { onIncomeAction: optionBtnGroupshandle, onShareAction: optionBtnGroupshandle, Popconfirm, downloadTemplateReq }) : baseColumns;
       set_columns(enhanced as any);
       set_filterConf(strategy.filterConf);
-      // 结合策略按钮解析
+      // 结合策略按钮解析(直接使用刚计算的 perms,而不是 state)
       let showCalc = strategy.showCalcButton;
+      let showExport = false;
       if (typeof strategy.resolveButtons === 'function') {
-        const btns = strategy.resolveButtons({ pagePermissions });
+        const btns = strategy.resolveButtons({ pagePermissions: perms });
         if (typeof btns?.calculate === 'boolean') showCalc = btns.calculate;
+        if (typeof btns?.export === 'boolean') showExport = btns.export;
       }
       set_ifShowCalcBtn(showCalc);
+      set_ifShowExportBtn(showExport);
       set_scrollX(strategy.scrollX);
       // 让策略在装载期可调整列等
       if (typeof strategy.onMount === 'function') {
@@ -911,7 +992,7 @@ export default function calcPageTemplate() {
       }
       // 让策略可配置表格额外属性(如禁用分页等)
       try {
-        const extra = typeof strategy.tablePropsBuilder === 'function' ? strategy.tablePropsBuilder({ pagePermissions }) : undefined;
+        const extra = typeof strategy.tablePropsBuilder === 'function' ? strategy.tablePropsBuilder({ pagePermissions: perms }) : undefined;
         set_tableExtraProps(extra || {});
       } catch {
         set_tableExtraProps({});
@@ -927,15 +1008,16 @@ export default function calcPageTemplate() {
       'incomeCollection', 'costShare', 'projectCostCalc'
     ]);
     if (strategy && simpleKeys.has((calcPageKey as any))) {
-      set_tableDataFilterParams((prev: any) => ({ ...prev, computeDate, calcPageKey, reportType: prev.reportType }));
+      // 切换页面时重置筛选参数,避免携带上个页面的残留检索项
+      set_tableDataFilterParams({ computeDate, calcPageKey });
       return;
     }
     // 其他场景均已由策略处理,无需在此分支初始化
 
     if (currentTabKey && params.calcPageKey == 'wholeHospCostCalculate') {
-      set_tableDataFilterParams((prev: any) => ({ ...prev, reportType: currentTabKey }))
+      set_tableDataFilterParams({ computeDate, calcPageKey, reportType: currentTabKey } as any);
     } else {
-      set_tableDataFilterParams((prev: any) => ({ ...prev, computeDate, calcPageKey, reportType: prev.reportType }));
+      set_tableDataFilterParams({ computeDate, calcPageKey } as any);
     }
 
   }, [params, currentTabKey]);
@@ -999,8 +1081,8 @@ export default function calcPageTemplate() {
               <span className='btn' style={{ marginRight: 16 }} onClick={() => handleExpandNext()}>展开下一层</span>
             </>
           )}
-          {(((params.calcPageKey == 'deptDirectMedicalCost' || params.calcPageKey == 'deptFullDirectCost' || params.calcPageKey == 'clinicalDeptMedicalCost' || params.calcPageKey == 'clinicalDeptFullCost' || params.calcPageKey == 'clinicalDeptFullCostAnalysis' || params.calcPageKey == 'deptCostAllocationSummary' || params.calcPageKey == 'hospitalVisitCostComposition' || params.calcPageKey == 'hospitalDeptVisitCost' || params.calcPageKey == 'hospitalBedDayCostComposition' || params.calcPageKey == 'hospitalDeptBedDayCost' || params.calcPageKey == 'hospitalServiceProjectCost' || params.calcPageKey == 'medicalServiceCostDetail' || params.calcPageKey == 'diseaseCostDetail' || params.calcPageKey == 'diseaseCostCompositionDetail' || params.calcPageKey == 'deptDiseaseCostCompositionDetail' || params.calcPageKey == 'drgCostDetail' || params.calcPageKey == 'drgCostCompositionDetail' || params.calcPageKey == 'deptDrgCostCompositionDetail') || ((params.calcPageKey == 'projectCostCalc' || params.calcPageKey == 'wholeHospCostCalculate') && pagePermissions.has('export')))) && <a className='export' onClick={() => set_openProcessModal(true)}>导出</a>}
-          {(ifShowCalcBtn && ((params.calcPageKey == 'wholeHospCostCalculate' || params.calcPageKey == 'projectCostCalc') ? pagePermissions.has('calculate') : true)) && <a className='calc' onClick={() => calcFunc()}>计算</a>}
+          {(((params.calcPageKey == 'deptDirectMedicalCost' || params.calcPageKey == 'deptFullDirectCost' || params.calcPageKey == 'clinicalDeptMedicalCost' || params.calcPageKey == 'clinicalDeptFullCost' || params.calcPageKey == 'clinicalDeptFullCostAnalysis' || params.calcPageKey == 'deptCostAllocationSummary' || params.calcPageKey == 'hospitalVisitCostComposition' || params.calcPageKey == 'hospitalDeptVisitCost' || params.calcPageKey == 'hospitalBedDayCostComposition' || params.calcPageKey == 'hospitalDeptBedDayCost' || params.calcPageKey == 'hospitalServiceProjectCost' || params.calcPageKey == 'medicalServiceCostDetail' || params.calcPageKey == 'diseaseCostDetail' || params.calcPageKey == 'diseaseCostCompositionDetail' || params.calcPageKey == 'deptDiseaseCostCompositionDetail' || params.calcPageKey == 'drgCostDetail' || params.calcPageKey == 'drgCostCompositionDetail' || params.calcPageKey == 'deptDrgCostCompositionDetail') || (params.calcPageKey == 'projectCostCalc' && ifShowExportBtn) || (params.calcPageKey == 'wholeHospCostCalculate' && pagePermissions.has('export')))) && <a className='export' onClick={() => set_openProcessModal(true)}>导出</a>}
+          {((params.calcPageKey == 'wholeHospCostCalculate') || ifShowCalcBtn) && <a className='calc' onClick={() => calcFunc()}>计算</a>}
           {(params.calcPageKey == 'afterCostShareSearch' || params.calcPageKey == 'afterCollectionSearch') && (<Button loading={loading} size='small' className='reportDataBtn' onClick={() => openDataTable()}>报表数据</Button>)}
           {(params.calcPageKey == 'departmentOperatingReport' || params.calcPageKey == 'wholeHospOperatingReport') && (<span className='reportDataBtn' onClick={() => generateReport()}>生成报表</span>)}
         </div>
@@ -1013,131 +1095,146 @@ export default function calcPageTemplate() {
           const { scroll: _omitScroll, ...restTableProps } = (tableExtraProps || {}) as any;
           const mergedY = extraScroll?.y ?? (params.calcPageKey == 'afterCostShareSearch' ? `calc(100vh - 270px)` : `calc(100vh - 233px)`);
 
-          // 仅在 deptDiseaseCostCompositionDetail 页面启用行虚拟化
-          const enableVirtual = params.calcPageKey === 'deptDiseaseCostCompositionDetail'
+          // 在大表启用行虚拟化(不分页):deptDiseaseCostCompositionDetail
+          // diseaseCostDetail 和 diseaseCostCompositionDetail 已改为后端分页,不再使用行虚拟化
+          const enableVirtual = (params.calcPageKey === 'deptDiseaseCostCompositionDetail')
             && typeof scrollX === 'number'
             && typeof mergedY === 'number'
-            && Array.isArray(columns) && columns.length > 0;
+            && Array.isArray(columns) && columns.length > 0
+            // 若存在固定列,则关闭行虚拟化以保留 fixed 效果
+            && !(columns as any[])?.some?.((c: any) => Boolean(c?.fixed));
 
           const virtualComponents = enableVirtual ? {
             body: buildVirtualBody({ columns: columns as any, height: mergedY as number, width: scrollX as number, rowHeight: 44 })
           } : undefined;
           const renderColumns = enableVirtual ? (columns as any[]).map((c) => ({ ...c, fixed: undefined })) : columns;
           return (
-        <KCIMTable
-          columns={renderColumns}
-          actionRef={tableRef}
-          rowKey="id"
-          components={virtualComponents as any}
-          expandable={params.calcPageKey == 'wholeHospCostCalculate' ? {
-            defaultExpandAllRows: true, 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]);
-              }
-            },
-          } : undefined}
-          rowClassName={(record) => {
-            // 全院损益树表的行 class
-            if (params.calcPageKey == 'wholeHospCostCalculate') {
-              return record.children ? 'has-children hover-row' : 'hover-row';
-            }
-            // 医院科室直接成本表/成本分摊汇总表:小计行高亮
-            if (params.calcPageKey == 'deptDirectMedicalCost' || params.calcPageKey == 'deptCostAllocationSummary') {
-              const name = String(record.responsibilityName || '');
-              return name.includes('小计') ? 'subtotal-row' : '';
-            }
-            return '';
-          }}
-          scroll={{ x: scrollX, y: mergedY }}
-          params={tableDataFilterParams}
-          request={(params) => getTableData(params)}
-          pagination={params.calcPageKey == 'wholeHospCostCalculate' ? false : (params.calcPageKey == 'deptDirectMedicalCost' ? false : undefined)}
-          {...restTableProps}
-          summary={(pageData: any[]) => {
-            // 仅对 deptDirectMedicalCost / deptCostAllocationSummary 固定底部合计
-            if (params.calcPageKey !== 'deptDirectMedicalCost' && params.calcPageKey !== 'deptCostAllocationSummary') return undefined;
-            // 由于该页不分页,这里的合计为当前渲染数据的总计
-            try {
-              // 如果接口有“总计”行,优先用它并固定到底部
-              if (bottomSummaryRow) {
-                return (
-                  <Table.Summary fixed>
-                    <Table.Summary.Row className="total-summary-row">
-                      <Table.Summary.Cell index={0}>总计</Table.Summary.Cell>
-                      {params.calcPageKey === 'deptDirectMedicalCost' ? (
-                        <>
-                          <Table.Summary.Cell index={1} align="right">{formatMoneyNumber(bottomSummaryRow.personnelExpense, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: bottomSummaryRow?.permil === 1 || bottomSummaryRow?.permil === true })}</Table.Summary.Cell>
-                          <Table.Summary.Cell index={2} align="right">{formatMoneyNumber(bottomSummaryRow.healthMaterialFee, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: bottomSummaryRow?.permil === 1 || bottomSummaryRow?.permil === true })}</Table.Summary.Cell>
-                          <Table.Summary.Cell index={3} align="right">{formatMoneyNumber(bottomSummaryRow.drugFee, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: bottomSummaryRow?.permil === 1 || bottomSummaryRow?.permil === true })}</Table.Summary.Cell>
-                          <Table.Summary.Cell index={4} align="right">{formatMoneyNumber(bottomSummaryRow.fixedAssetDepreciation, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: bottomSummaryRow?.permil === 1 || bottomSummaryRow?.permil === true })}</Table.Summary.Cell>
-                          <Table.Summary.Cell index={5} align="right">{formatMoneyNumber(bottomSummaryRow.intangibleAssetAmortization, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: bottomSummaryRow?.permil === 1 || bottomSummaryRow?.permil === true })}</Table.Summary.Cell>
-                          <Table.Summary.Cell index={6} align="right">{formatMoneyNumber(bottomSummaryRow.medicalRiskFundExtraction, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: bottomSummaryRow?.permil === 1 || bottomSummaryRow?.permil === true })}</Table.Summary.Cell>
-                          <Table.Summary.Cell index={7} align="right">{formatMoneyNumber(bottomSummaryRow.otherMedicalExpenses, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: bottomSummaryRow?.permil === 1 || bottomSummaryRow?.permil === true })}</Table.Summary.Cell>
-                          <Table.Summary.Cell index={8} align="right">{formatMoneyNumber(bottomSummaryRow.total, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: bottomSummaryRow?.permil === 1 || bottomSummaryRow?.permil === true })}</Table.Summary.Cell>
-                        </>
-                      ) : (
-                        <>
-                          <Table.Summary.Cell index={1} align="right">{formatMoneyNumber(bottomSummaryRow.medicalCost, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                          <Table.Summary.Cell index={2} align="right">{formatMoneyNumber(bottomSummaryRow.directCost, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                          <Table.Summary.Cell index={3} align="right">{formatMoneyNumber(bottomSummaryRow.subtotal, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                          <Table.Summary.Cell index={4} align="right">{formatMoneyNumber(bottomSummaryRow.allocatedAdminCost, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                          <Table.Summary.Cell index={5} align="right">{formatMoneyNumber(bottomSummaryRow.allocatedSupportCost, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                          <Table.Summary.Cell index={6} align="right">{formatMoneyNumber(bottomSummaryRow.allocatedTechCost, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                        </>
-                      )}
-                    </Table.Summary.Row>
-                  </Table.Summary>
-                );
-              }
-              // 数值列 key 列表
-              const amountKeys = params.calcPageKey === 'deptDirectMedicalCost'
-                ? ['personnelExpense','healthMaterialFee','drugFee','fixedAssetDepreciation','intangibleAssetAmortization','medicalRiskFundExtraction','otherMedicalExpenses','total']
-                : ['medicalCost','directCost','subtotal','allocatedAdminCost','allocatedSupportCost','allocatedTechCost'];
-              const sums: Record<string, number> = {};
-              (Array.isArray(pageData) ? pageData : [])?.forEach?.((row: any) => {
-                amountKeys.forEach(k => {
-                  const v = Number(row?.[k] ?? 0);
-                  sums[k] = (sums[k] || 0) + (isNaN(v) ? 0 : v);
-                })
-              });
-              return (
-                <Table.Summary fixed>
-                  <Table.Summary.Row className="total-summary-row">
-                    <Table.Summary.Cell index={0}>合计</Table.Summary.Cell>
-                    {params.calcPageKey === 'deptDirectMedicalCost' ? (
-                      <>
-                        <Table.Summary.Cell index={1} align="right">{formatMoneyNumber(sums.personnelExpense, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                        <Table.Summary.Cell index={2} align="right">{formatMoneyNumber(sums.healthMaterialFee, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                        <Table.Summary.Cell index={3} align="right">{formatMoneyNumber(sums.drugFee, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                        <Table.Summary.Cell index={4} align="right">{formatMoneyNumber(sums.fixedAssetDepreciation, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                        <Table.Summary.Cell index={5} align="right">{formatMoneyNumber(sums.intangibleAssetAmortization, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                        <Table.Summary.Cell index={6} align="right">{formatMoneyNumber(sums.medicalRiskFundExtraction, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                        <Table.Summary.Cell index={7} align="right">{formatMoneyNumber(sums.otherMedicalExpenses, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                        <Table.Summary.Cell index={8} align="right">{formatMoneyNumber(sums.total, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                      </>
-                    ) : (
-                      <>
-                        <Table.Summary.Cell index={1} align="right">{formatMoneyNumber(sums.medicalCost, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                        <Table.Summary.Cell index={2} align="right">{formatMoneyNumber(sums.directCost, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                        <Table.Summary.Cell index={3} align="right">{formatMoneyNumber(sums.subtotal, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                        <Table.Summary.Cell index={4} align="right">{formatMoneyNumber(sums.allocatedAdminCost, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                        <Table.Summary.Cell index={5} align="right">{formatMoneyNumber(sums.allocatedSupportCost, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                        <Table.Summary.Cell index={6} align="right">{formatMoneyNumber(sums.allocatedTechCost, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
-                      </>
-                    )}
-                  </Table.Summary.Row>
-                </Table.Summary>
-              );
-            } catch {
-              return undefined;
-            }
-          }}
-        />)})()}
+            <KCIMTable
+              columns={renderColumns}
+              actionRef={tableRef}
+              rowKey="id"
+              components={virtualComponents as any}
+              expandable={params.calcPageKey == 'wholeHospCostCalculate' ? {
+                defaultExpandAllRows: true, 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]);
+                  }
+                },
+              } : (params.calcPageKey == 'hospitalServiceProjectCost' ? {
+                expandedRowKeys: hospitalServiceProjectExpandedKeys,
+                onExpand(expanded, record) {
+                  const { id } = record;
+                  if (!expanded) {
+                    const expandedKeys = hospitalServiceProjectExpandedKeys.filter(a => a != id);
+                    set_hospitalServiceProjectExpandedKeys([...expandedKeys]);
+                  } else {
+                    set_hospitalServiceProjectExpandedKeys([...hospitalServiceProjectExpandedKeys, id]);
+                  }
+                },
+              } : undefined)}
+              rowClassName={(record) => {
+                // 全院损益树表的行 class
+                if (params.calcPageKey == 'wholeHospCostCalculate') {
+                  return record.children ? 'has-children hover-row' : 'hover-row';
+                }
+                // 医院科室直接成本表/成本分摊汇总表:小计行高亮
+                if (params.calcPageKey == 'deptDirectMedicalCost' || params.calcPageKey == 'deptCostAllocationSummary') {
+                  const name = String(record.responsibilityName || '');
+                  return name.includes('小计') ? 'subtotal-row' : '';
+                }
+                return '';
+              }}
+              scroll={{ x: scrollX, y: mergedY }}
+              params={tableDataFilterParams}
+              request={(params) => getTableData(params)}
+              pagination={params.calcPageKey == 'wholeHospCostCalculate' ? false : (params.calcPageKey == 'deptDirectMedicalCost' ? false : undefined)}
+              {...restTableProps}
+              summary={(pageData: any[]) => {
+                // 仅对 deptDirectMedicalCost / deptCostAllocationSummary 固定底部合计
+                if (params.calcPageKey !== 'deptDirectMedicalCost' && params.calcPageKey !== 'deptCostAllocationSummary') return undefined;
+                // 由于该页不分页,这里的合计为当前渲染数据的总计
+                try {
+                  // 如果接口有“总计”行,优先用它并固定到底部
+                  if (bottomSummaryRow) {
+                    return (
+                      <Table.Summary fixed>
+                        <Table.Summary.Row className="total-summary-row">
+                          <Table.Summary.Cell index={0}>总计</Table.Summary.Cell>
+                          {params.calcPageKey === 'deptDirectMedicalCost' ? (
+                            <>
+                              <Table.Summary.Cell index={1} align="right">{formatMoneyNumber(bottomSummaryRow.personnelExpense, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: bottomSummaryRow?.permil === 1 || bottomSummaryRow?.permil === true })}</Table.Summary.Cell>
+                              <Table.Summary.Cell index={2} align="right">{formatMoneyNumber(bottomSummaryRow.healthMaterialFee, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: bottomSummaryRow?.permil === 1 || bottomSummaryRow?.permil === true })}</Table.Summary.Cell>
+                              <Table.Summary.Cell index={3} align="right">{formatMoneyNumber(bottomSummaryRow.drugFee, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: bottomSummaryRow?.permil === 1 || bottomSummaryRow?.permil === true })}</Table.Summary.Cell>
+                              <Table.Summary.Cell index={4} align="right">{formatMoneyNumber(bottomSummaryRow.fixedAssetDepreciation, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: bottomSummaryRow?.permil === 1 || bottomSummaryRow?.permil === true })}</Table.Summary.Cell>
+                              <Table.Summary.Cell index={5} align="right">{formatMoneyNumber(bottomSummaryRow.intangibleAssetAmortization, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: bottomSummaryRow?.permil === 1 || bottomSummaryRow?.permil === true })}</Table.Summary.Cell>
+                              <Table.Summary.Cell index={6} align="right">{formatMoneyNumber(bottomSummaryRow.medicalRiskFundExtraction, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: bottomSummaryRow?.permil === 1 || bottomSummaryRow?.permil === true })}</Table.Summary.Cell>
+                              <Table.Summary.Cell index={7} align="right">{formatMoneyNumber(bottomSummaryRow.otherMedicalExpenses, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: bottomSummaryRow?.permil === 1 || bottomSummaryRow?.permil === true })}</Table.Summary.Cell>
+                              <Table.Summary.Cell index={8} align="right">{formatMoneyNumber(bottomSummaryRow.total, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: bottomSummaryRow?.permil === 1 || bottomSummaryRow?.permil === true })}</Table.Summary.Cell>
+                            </>
+                          ) : (
+                            <>
+                              <Table.Summary.Cell index={1} align="right">{formatMoneyNumber(bottomSummaryRow.medicalCost, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                              <Table.Summary.Cell index={2} align="right">{formatMoneyNumber(bottomSummaryRow.directCost, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                              <Table.Summary.Cell index={3} align="right">{formatMoneyNumber(bottomSummaryRow.subtotal, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                              <Table.Summary.Cell index={4} align="right">{formatMoneyNumber(bottomSummaryRow.allocatedAdminCost, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                              <Table.Summary.Cell index={5} align="right">{formatMoneyNumber(bottomSummaryRow.allocatedSupportCost, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                              <Table.Summary.Cell index={6} align="right">{formatMoneyNumber(bottomSummaryRow.allocatedTechCost, { decimalPlaces: (typeof bottomSummaryRow?.decimalPlace === 'number') ? bottomSummaryRow.decimalPlace : 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                            </>
+                          )}
+                        </Table.Summary.Row>
+                      </Table.Summary>
+                    );
+                  }
+                  // 数值列 key 列表
+                  const amountKeys = params.calcPageKey === 'deptDirectMedicalCost'
+                    ? ['personnelExpense', 'healthMaterialFee', 'drugFee', 'fixedAssetDepreciation', 'intangibleAssetAmortization', 'medicalRiskFundExtraction', 'otherMedicalExpenses', 'total']
+                    : ['medicalCost', 'directCost', 'subtotal', 'allocatedAdminCost', 'allocatedSupportCost', 'allocatedTechCost'];
+                  const sums: Record<string, number> = {};
+                  (Array.isArray(pageData) ? pageData : [])?.forEach?.((row: any) => {
+                    amountKeys.forEach(k => {
+                      const v = Number(row?.[k] ?? 0);
+                      sums[k] = (sums[k] || 0) + (isNaN(v) ? 0 : v);
+                    })
+                  });
+                  return (
+                    <Table.Summary fixed>
+                      <Table.Summary.Row className="total-summary-row">
+                        <Table.Summary.Cell index={0}>合计</Table.Summary.Cell>
+                        {params.calcPageKey === 'deptDirectMedicalCost' ? (
+                          <>
+                            <Table.Summary.Cell index={1} align="right">{formatMoneyNumber(sums.personnelExpense, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                            <Table.Summary.Cell index={2} align="right">{formatMoneyNumber(sums.healthMaterialFee, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                            <Table.Summary.Cell index={3} align="right">{formatMoneyNumber(sums.drugFee, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                            <Table.Summary.Cell index={4} align="right">{formatMoneyNumber(sums.fixedAssetDepreciation, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                            <Table.Summary.Cell index={5} align="right">{formatMoneyNumber(sums.intangibleAssetAmortization, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                            <Table.Summary.Cell index={6} align="right">{formatMoneyNumber(sums.medicalRiskFundExtraction, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                            <Table.Summary.Cell index={7} align="right">{formatMoneyNumber(sums.otherMedicalExpenses, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                            <Table.Summary.Cell index={8} align="right">{formatMoneyNumber(sums.total, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                          </>
+                        ) : (
+                          <>
+                            <Table.Summary.Cell index={1} align="right">{formatMoneyNumber(sums.medicalCost, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                            <Table.Summary.Cell index={2} align="right">{formatMoneyNumber(sums.directCost, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                            <Table.Summary.Cell index={3} align="right">{formatMoneyNumber(sums.subtotal, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                            <Table.Summary.Cell index={4} align="right">{formatMoneyNumber(sums.allocatedAdminCost, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                            <Table.Summary.Cell index={5} align="right">{formatMoneyNumber(sums.allocatedSupportCost, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                            <Table.Summary.Cell index={6} align="right">{formatMoneyNumber(sums.allocatedTechCost, { decimalPlaces: 2, useThousandSeparator: true })}</Table.Summary.Cell>
+                          </>
+                        )}
+                      </Table.Summary.Row>
+                    </Table.Summary>
+                  );
+                } catch {
+                  return undefined;
+                }
+              }}
+            />)
+        })()}
         {
           totalCount != undefined && <a style={{ marginTop: 16, display: 'inline-block' }}>{`合计:${formatMoneyNumber(totalCount)}`}</a>
         }

+ 3 - 3
src/pages/costAccounting/calcPageTemplate/service.ts

@@ -252,7 +252,7 @@ export const getCalcPageTableData = (params: any) => {
   }
   // 医院病种成本明细表
   if(calcPageKey == 'diseaseCostDetail'){
-    return request('/costAccount/standardReport/getDiseaseCostDetail', {
+    return request('/costAccount/standardReport/getDiseaseCostDetailByPage', {
       method: 'GET',
       params:{...next}
     });
@@ -273,14 +273,14 @@ export const getCalcPageTableData = (params: any) => {
   }
   // 病种成本构成明细表
   if(calcPageKey == 'diseaseCostCompositionDetail'){
-    return request('/costAccount/standardReport/getDiseaseCostCompositionDetail', {
+    return request('/costAccount/standardReport/getDiseaseCostCompositionDetailByPage', {
       method: 'GET',
       params:{...next}
     });
   }
   // 服务单元病种成本构成明细表
   if(calcPageKey == 'deptDiseaseCostCompositionDetail'){
-    return request('/costAccount/standardReport/getDeptDiseaseCostCompositionDetail', {
+    return request('/costAccount/standardReport/getDeptDiseaseCostCompositionDetailByPage', {
       method: 'GET',
       params:{...next}
     });

+ 44 - 17
src/pages/costAccounting/calcPageTemplate/strategy.tsx

@@ -53,6 +53,8 @@ import {
   deptFullDirectCostFilterConf,
   clinicalDeptMedicalCostFilterConf,
   clinicalDeptFullCostFilterConf,
+  medicalServiceCostDetailFilterConf,
+  hospitalServiceProjectCostFilterConf,
 } from './config';
 import { getParamsDataBySysId } from '@/services/getDic';
 
@@ -232,7 +234,7 @@ const STRATEGIES: Record<string, CalcPageStrategy> = {
           const filtered = (ctx.columns as ProColumns[]).filter((c: any) => c?.title !== '占比');
           ctx.setColumns && ctx.setColumns(filtered);
         }
-      } catch {}
+      } catch { }
     }
   },
   costShareReportTable: {
@@ -253,7 +255,7 @@ const STRATEGIES: Record<string, CalcPageStrategy> = {
             <a style={{ fontSize: 14 }} onClick={async () => {
               const { year, month, shareLevel, shareLevelId } = record;
               await downloadTemplateReq('/costAccount/excel/getShareReportTemplate', { year, month, levelSort: shareLevel, shareLevelId })
-                .catch(() => {});
+                .catch(() => { });
             }}>下载</a>,
           ],
         } as ProColumns,
@@ -339,10 +341,11 @@ const STRATEGIES: Record<string, CalcPageStrategy> = {
     showCalcButton: false,
     resolveButtons: (ctx: any) => {
       try {
-        const has = ctx?.pagePermissions?.has?.('calculate');
-        return { calculate: !!has };
+        const hasCalculate = ctx?.pagePermissions?.has?.('calculate');
+        const hasExport = ctx?.pagePermissions?.has?.('export');
+        return { calculate: !!hasCalculate, export: !!hasExport };
       } catch {
-        return { calculate: false };
+        return { calculate: false, export: false };
       }
     },
   },
@@ -353,7 +356,7 @@ const STRATEGIES: Record<string, CalcPageStrategy> = {
     filterConf: deptDirectMedicalCostFilterConf,
     scrollX: 1450,
     showCalcButton: false,
-    tablePropsBuilder: () => ({ pagination: false,scroll: { y:662 } }),
+    tablePropsBuilder: () => ({ pagination: false, scroll: { y: 662 } }),
   },
   // 医院科室直接成本表(全成本)
   deptFullDirectCost: {
@@ -448,7 +451,7 @@ const STRATEGIES: Record<string, CalcPageStrategy> = {
   hospitalServiceProjectCost: {
     key: 'hospitalServiceProjectCost',
     buildColumns: () => (require('./columns') as any).hospitalServiceProjectCost,
-    filterConf: [],
+    filterConf: hospitalServiceProjectCostFilterConf,
     scrollX: 1200,
     showCalcButton: false,
     tablePropsBuilder: () => ({ pagination: false, scroll: { y: 700 } }),
@@ -457,32 +460,48 @@ const STRATEGIES: Record<string, CalcPageStrategy> = {
   medicalServiceCostDetail: {
     key: 'medicalServiceCostDetail',
     buildColumns: () => (require('./columns') as any).medicalServiceCostDetail,
-    filterConf: [],
+    filterConf: medicalServiceCostDetailFilterConf,
     scrollX: 1200,
     showCalcButton: false,
     tablePropsBuilder: () => ({ pagination: false, scroll: { y: 661 } }),
   },
-  // 医院病种成本明细表
+  // 医院病种成本明细表(后端分页+检索)
   diseaseCostDetail: {
     key: 'diseaseCostDetail',
     buildColumns: () => (require('./columns') as any).diseaseCostDetail,
     filterConf: [
-      { type: 'input', label: '病种名称:', placeholder: '请输入', key: 'itemName' }
+      { type: 'input', label: '病种名称:', placeholder: '请输入', key: 'diseaseFilter' }
     ],
     scrollX: 1200,
     showCalcButton: false,
-    tablePropsBuilder: () => ({ pagination: false, scroll: { y: 661 } }),
-  },
-  // 病种成本构成明细表
+    // 启用分页,关闭行虚拟化
+    tablePropsBuilder: () => ({
+      pagination: {
+        showSizeChanger: true,
+        defaultPageSize: 20,
+        pageSizeOptions: ['10', '20', '50', '100']
+      },
+      scroll: { y: 600 }
+    }),
+  },
+  // 病种成本构成明细表(后端分页+检索)
   diseaseCostCompositionDetail: {
     key: 'diseaseCostCompositionDetail',
     buildColumns: () => (require('./columns') as any).diseaseCostCompositionDetail,
     filterConf: [
-      { type: 'input', label: '病种名称:', placeholder: '请输入', key: 'itemName' }
+      { type: 'input', label: '病种名称:', placeholder: '请输入', key: 'diseaseFilter' }
     ],
     scrollX: 2200,
     showCalcButton: false,
-    tablePropsBuilder: () => ({ pagination: false, scroll: { y: 661 } }),
+    // 启用分页,关闭行虚拟化
+    tablePropsBuilder: () => ({
+      pagination: {
+        showSizeChanger: true,
+        defaultPageSize: 20,
+        pageSizeOptions: ['10', '20', '50', '100']
+      },
+      scroll: { y: 600 }
+    }),
   },
   // 医院DRG成本明细表
   drgCostDetail: {
@@ -523,11 +542,19 @@ const STRATEGIES: Record<string, CalcPageStrategy> = {
     // 列由接口 title 动态生成,这里返回空数组占位
     buildColumns: () => [],
     filterConf: [
-      { type: 'input', label: '病种名称:', placeholder: '请输入', key: 'itemName' }
+      { type: 'input', label: '病种名称:', placeholder: '请输入', key: 'diseaseFilter' }
     ],
     scrollX: 1200,
     showCalcButton: false,
-    tablePropsBuilder: () => ({ pagination: false, scroll: { y: 661 } }),
+    // 启用分页,关闭行虚拟化
+    tablePropsBuilder: () => ({
+      pagination: {
+        showSizeChanger: true,
+        defaultPageSize: 20,
+        pageSizeOptions: ['10', '20', '50', '100']
+      },
+      scroll: { y: 600 }
+    }),
   },
 };
 

+ 9 - 5
src/pages/costAccounting/calcPageTemplate/style.less

@@ -74,11 +74,7 @@
       }
     }
 
-    .cost-ant-table-header {
-      .cost-ant-table-thead {
-        .cost-ant-table-cell {}
-      }
-    }
+    // 删除空规则,避免linter提示
 
     .cost-ant-table-body {
       .cost-ant-table-row {
@@ -90,4 +86,12 @@
       }
     }
   }
+}
+
+// 总计行样式:字体颜色与普通行一致对齐,颜色 #296DCC
+.total-summary-row {
+  td {
+    color: #296DCC !important;
+    font-weight: 600;
+  }
 }

+ 16 - 0
src/pages/costAccounting/calcPageTemplate/usePageAccess.ts

@@ -0,0 +1,16 @@
+/*
+ * 按页面解析按钮权限的封装
+ */
+
+export function getPagePermissions(access: any, pathname: string): Set<string> {
+  try {
+    if (!access || typeof access.whatCanIDoInThisPage !== 'function') return new Set();
+    const tabs = access.whatCanIDoInThisPage(pathname);
+    const codes = Array.isArray(tabs) ? tabs.map((t: any) => t.code) : [];
+    return new Set(codes);
+  } catch {
+    return new Set();
+  }
+}
+
+

+ 37 - 0
src/pages/costAccounting/calcPageTemplate/usePagedExporter.ts

@@ -0,0 +1,37 @@
+export interface PagedExporterOptions<TParams> {
+  pageSize?: number;
+  request: (params: TParams & { current: number; pageSize: number }) => Promise<{ data: any[]; total: number } | undefined>;
+}
+
+export interface PagedExporterResult {
+  fetchData: () => Promise<{ currentCount: number; totalCount: number }>;
+  reset: () => void;
+  getAll: () => any[];
+}
+
+// 可复用的分页导出 Hook(无 React 依赖,简单状态机)
+export function createPagedExporter<TParams = any>(options: PagedExporterOptions<TParams>) {
+  const state = {
+    current: 0,
+    totalData: [] as any[],
+    pageSize: options.pageSize || 100,
+  };
+
+  const fetchData = async (passThroughParams: TParams): Promise<{ currentCount: number; totalCount: number }> => {
+    state.current += 1;
+    const resp = await options.request({ ...(passThroughParams as any), current: state.current, pageSize: state.pageSize });
+    const total = resp?.total || 0;
+    const data = resp?.data || [];
+    state.totalData = [...state.totalData, ...data];
+    return { currentCount: state.totalData.length, totalCount: total };
+  };
+
+  const reset = () => {
+    state.current = 0;
+    state.totalData = [];
+  };
+
+  const getAll = () => state.totalData;
+
+  return { fetchData, reset, getAll } as const;
+}

+ 576 - 70
src/pages/departmentCostCheck/index.tsx

@@ -2,7 +2,7 @@
  * @Author: code4eat awesomedema@gmail.com
  * @Date: 2023-03-03 11:30:33
  * @LastEditors: code4eat awesomedema@gmail.com
- * @LastEditTime: 2025-05-13 10:26:40
+ * @LastEditTime: 2025-08-01 16:40:51
  * @FilePath: /KC-MiddlePlatform/src/pages/platform/setting/pubDicTypeMana/index.tsx
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */
@@ -28,7 +28,7 @@ import locale from 'antd/es/date-picker/locale/zh_CN';
 
 
 
-import { computeProfitReq, getNextLevelTableData, getReportDataReq, getReportProjectSettingList, getResponsibleCenters } from './service';
+import { computeProfitReq, getNextLevelTableData, getReportDataReq, getReportProjectSettingList, getResponsibleCenters, getBatchComputeProfitListByYear } from './service';
 
 import './style.less';
 import React from 'react';
@@ -39,6 +39,7 @@ import { formatMoneyNumber } from '@/utils/format';
 import { useModel } from '@umijs/max';
 import ReportExport from '../reportExport/report';
 import { getUserHasReports } from '../costAccounting/calcPageTemplate/service';
+import { useLocation } from '@umijs/max';
 
 
 const { RangePicker } = DatePicker;
@@ -91,43 +92,64 @@ const transformData = (data: any[]) => {
     const resultMap: Map<string, { reportName: string; key: string; children?: any[];[key: string]: any }> = new Map();
 
     // 转换单个条目
-    const transformEntry = (entry: any, computeDate: string) => {
+    const transformEntry = (entry: any, computeDate: string, allowDrillDown?: boolean) => {
         if (!entry) return {};
         const transformedEntry = { ...entry };
         
-        // 根据 dataType 处理金额
-        if (entry.dataType === 2) {
-            // 百分比类型
-            transformedEntry[`${computeDate}_amount`] = entry.amount != null ? `${((entry.amount * 100).toFixed(entry.decimalPlace || 2))}%` : null;
-        } else {
-            // 数值类型 (dataType === 1 或未定义)
-            transformedEntry[`${computeDate}_amount`] = entry.amount != null ? formatMoneyNumber(entry.amount, {
-                decimalPlaces: entry.decimalPlace,
-                useThousandSeparator: Boolean(entry.permil)
-            }) : null;
-        }
+        // 根据 dataType 处理不同类型的数据
+        const formatValue = (value: number) => {
+            if (value == null) return null;
+            
+            if (entry.dataType === 2) {
+                // 百分比类型
+                return `${((value * 100).toFixed(entry.decimalPlace || 2))}%`;
+            } else {
+                // 数值类型 (dataType === 1 或未定义)
+                return formatMoneyNumber(value, {
+                    decimalPlaces: entry.decimalPlace,
+                    useThousandSeparator: Boolean(entry.permil)
+                });
+            }
+        };
         
+        // 处理各个字段
+        transformedEntry[`${computeDate}_amount`] = formatValue(entry.amount);
+        transformedEntry[`${computeDate}_budgetAmount`] = formatValue(entry.budgetAmount);
+        transformedEntry[`${computeDate}_prevPeriodAmount`] = formatValue(entry.prevPeriodAmount);
+        transformedEntry[`${computeDate}_samePeriodAmount`] = formatValue(entry.samePeriodAmount);
+        
+        // 百分比字段始终显示为百分比
         transformedEntry[`${computeDate}_percent`] = entry.percent != null ? `${((entry.percent * 100).toFixed(2))}%` : null;
+        transformedEntry[`${computeDate}_completionRate`] = entry.completionRate != null ? 
+            `${((entry.completionRate * 100).toFixed(entry.decimalPlace || 2))}%` : null;
+        transformedEntry[`${computeDate}_momRate`] = entry.momRate != null ? 
+            `${((entry.momRate * 100).toFixed(entry.decimalPlace || 2))}%` : null;
+        transformedEntry[`${computeDate}_yoyRate`] = entry.yoyRate != null ? 
+            `${((entry.yoyRate * 100).toFixed(entry.decimalPlace || 2))}%` : null;
+        
+        // 处理allowDrillDown字段,按月份字段化
+        transformedEntry[`${computeDate}_allowDrillDown`] = allowDrillDown;
+        
         return transformedEntry;
     };
 
     // 合并新的条目到现有条目,不覆盖已有的字段
     const mergeEntries = (existingEntry: any, newEntry: any) => {
         for (const key in newEntry) {
-            if (!existingEntry[key] || key.endsWith('_amount') || key.endsWith('_percent')) {
+            if (!existingEntry[key] || key.endsWith('_amount') || key.endsWith('_percent') || key.endsWith('_allowDrillDown')) {
                 existingEntry[key] = newEntry[key];
             }
         }
     };
 
     // 递归处理每个节点及其子节点
-    const processNode = (node: any, computeDate: string): any => {
+    const processNode = (node: any, computeDate: string, allowDrillDown?: boolean): any => {
         if (!node) return {};
 
-        const transformedNode = transformEntry(node, computeDate);
+        const transformedNode = transformEntry(node, computeDate, allowDrillDown);
 
         if (node.children && Array.isArray(node.children)) {
-            const processedChildren = node.children.map((child: any) => processNode(child, computeDate));
+            const processedChildren = node.children.map((child: any) => processNode(child, computeDate, allowDrillDown));
             transformedNode.children = mergeChildren(transformedNode.children || [], processedChildren);
         }
 
@@ -154,7 +176,7 @@ const transformData = (data: any[]) => {
 
     // 主处理逻辑
     data.forEach((item) => {
-        const { computeDate, profitVoList } = item;
+        const { computeDate, profitVoList, allowDrillDown } = item;
 
         if (!profitVoList || !Array.isArray(profitVoList)) {
             return;
@@ -163,13 +185,13 @@ const transformData = (data: any[]) => {
         profitVoList.forEach((profit) => {
             if (!resultMap.has(profit.reportName)) {
                 resultMap.set(profit.reportName, {
-                    ...transformEntry(profit, computeDate),
+                    ...transformEntry(profit, computeDate, allowDrillDown),
                     key: profit.reportName,
                 });
             }
 
             const existingEntry = resultMap.get(profit.reportName)!;
-            const transformedProfit = processNode(profit, computeDate);
+            const transformedProfit = processNode(profit, computeDate, allowDrillDown);
 
             mergeEntries(existingEntry, transformedProfit);
 
@@ -179,6 +201,8 @@ const transformData = (data: any[]) => {
         });
     });
 
+
+
     // 确保每个层级的数据都有完整的月份字段
     const fillMissingMonths = (nodes: any[], computeDates: string[]) => {
         nodes.forEach((node) => {
@@ -186,9 +210,30 @@ const transformData = (data: any[]) => {
                 if (!node.hasOwnProperty(`${date}_amount`)) {
                     node[`${date}_amount`] = node.amount != null ? formatMoneyNumber(node.amount,{decimalPlaces:node.decimalPlace,useThousandSeparator:Boolean(node.permil)}) : null;
                 }
+                if (!node.hasOwnProperty(`${date}_budgetAmount`)) {
+                    node[`${date}_budgetAmount`] = node.budgetAmount != null ? formatMoneyNumber(node.budgetAmount,{decimalPlaces:node.decimalPlace,useThousandSeparator:Boolean(node.permil)}) : null;
+                }
+                if (!node.hasOwnProperty(`${date}_prevPeriodAmount`)) {
+                    node[`${date}_prevPeriodAmount`] = node.prevPeriodAmount != null ? formatMoneyNumber(node.prevPeriodAmount,{decimalPlaces:node.decimalPlace,useThousandSeparator:Boolean(node.permil)}) : null;
+                }
+                if (!node.hasOwnProperty(`${date}_samePeriodAmount`)) {
+                    node[`${date}_samePeriodAmount`] = node.samePeriodAmount != null ? formatMoneyNumber(node.samePeriodAmount,{decimalPlaces:node.decimalPlace,useThousandSeparator:Boolean(node.permil)}) : null;
+                }
+                if (!node.hasOwnProperty(`${date}_completionRate`)) {
+                    node[`${date}_completionRate`] = node.completionRate != null ? `${((node.completionRate * 100).toFixed(node.decimalPlace || 2))}%` : null;
+                }
+                if (!node.hasOwnProperty(`${date}_momRate`)) {
+                    node[`${date}_momRate`] = node.momRate != null ? `${((node.momRate * 100).toFixed(node.decimalPlace || 2))}%` : null;
+                }
+                if (!node.hasOwnProperty(`${date}_yoyRate`)) {
+                    node[`${date}_yoyRate`] = node.yoyRate != null ? `${((node.yoyRate * 100).toFixed(node.decimalPlace || 2))}%` : null;
+                }
                 if (!node.hasOwnProperty(`${date}_percent`)) {
                     node[`${date}_percent`] = node.percent != null ? `${((node.percent * 100).toFixed(2))}%` : null;
                 }
+                if (!node.hasOwnProperty(`${date}_allowDrillDown`)) {
+                    node[`${date}_allowDrillDown`] = node.allowDrillDown;
+                }
             });
 
             if (node.children && Array.isArray(node.children)) {
@@ -216,6 +261,183 @@ const transformData = (data: any[]) => {
     return transformedData;
 };
 
+// 年度数据转换函数
+const transformYearData = (data: any[]) => {
+    const resultMap: Map<string, { reportName: string; key: string; children?: any[];[key: string]: any }> = new Map();
+
+    // 转换单个条目
+    const transformYearEntry = (entry: any, computeDate: string, allowDrillDown?: boolean) => {
+        if (!entry) return {};
+        const transformedEntry = { ...entry };
+        
+        // 根据 dataType 处理不同类型的数据
+        const formatValue = (value: number) => {
+            if (value == null) return null;
+            
+            if (entry.dataType === 2) {
+                // 百分比类型
+                return `${((value * 100).toFixed(entry.decimalPlace || 2))}%`;
+            } else {
+                // 数值类型 (dataType === 1 或未定义)
+                return formatMoneyNumber(value, {
+                    decimalPlaces: entry.decimalPlace,
+                    useThousandSeparator: Boolean(entry.permil)
+                });
+            }
+        };
+        
+        // 处理年度数据的各个字段
+        transformedEntry[`${computeDate}_amount`] = formatValue(entry.amount);
+        transformedEntry[`${computeDate}_budgetAmount`] = formatValue(entry.budgetAmount);
+        transformedEntry[`${computeDate}_samePeriodAmount`] = formatValue(entry.samePeriodAmount);
+        
+        // 完成率和同比始终显示为百分比
+        transformedEntry[`${computeDate}_completionRate`] = entry.completionRate != null ? 
+            `${((entry.completionRate * 100).toFixed(entry.decimalPlace || 2))}%` : null;
+        
+        transformedEntry[`${computeDate}_yoyRate`] = entry.yoyRate != null ? 
+            `${((entry.yoyRate * 100).toFixed(entry.decimalPlace || 2))}%` : null;
+        
+        // 处理allowDrillDown字段,按年份字段化
+        transformedEntry[`${computeDate}_allowDrillDown`] = allowDrillDown;
+        
+        return transformedEntry;
+    };
+
+    // 合并新的条目到现有条目
+    const mergeYearEntries = (existingEntry: any, newEntry: any) => {
+        for (const key in newEntry) {
+            if (!existingEntry[key] || key.includes('_amount') || key.includes('_budgetAmount') || 
+                key.includes('_samePeriodAmount') || key.includes('_completionRate') || key.includes('_yoyRate') || 
+                key.includes('_allowDrillDown')) {
+                existingEntry[key] = newEntry[key];
+            }
+        }
+    };
+
+    // 递归处理每个节点及其子节点
+    const processYearNode = (node: any, computeDate: string, allowDrillDown?: boolean): any => {
+        if (!node) return {};
+
+        const transformedNode = transformYearEntry(node, computeDate, allowDrillDown);
+
+        if (node.children && Array.isArray(node.children)) {
+            const processedChildren = node.children.map((child: any) => processYearNode(child, computeDate, allowDrillDown));
+            transformedNode.children = mergeYearChildren(transformedNode.children || [], processedChildren);
+        }
+
+        return transformedNode;
+    };
+
+    // 合并子节点
+    const mergeYearChildren = (existingChildren: any[], newChildren: any[]) => {
+        newChildren.forEach((newChild) => {
+            const existingChild = existingChildren.find((child) => child.reportName === newChild.reportName && child.key === newChild.key);
+
+            if (existingChild) {
+                mergeYearEntries(existingChild, newChild);
+                if (newChild.children && Array.isArray(newChild.children)) {
+                    existingChild.children = mergeYearChildren(existingChild.children || [], newChild.children);
+                }
+            } else {
+                existingChildren.push(newChild);
+            }
+        });
+
+        return existingChildren;
+    };
+
+    // 主处理逻辑
+    data.forEach((item) => {
+        const { computeDate, profitVoList, allowDrillDown } = item;
+
+        if (!profitVoList || !Array.isArray(profitVoList)) {
+            return;
+        }
+
+        profitVoList.forEach((profit) => {
+            if (!resultMap.has(profit.reportName)) {
+                resultMap.set(profit.reportName, {
+                    ...transformYearEntry(profit, computeDate, allowDrillDown),
+                    key: profit.reportName,
+                });
+            }
+
+            const existingEntry = resultMap.get(profit.reportName)!;
+            const transformedProfit = processYearNode(profit, computeDate, allowDrillDown);
+
+            mergeYearEntries(existingEntry, transformedProfit);
+
+            if (transformedProfit.children && Array.isArray(transformedProfit.children)) {
+                existingEntry.children = mergeYearChildren(existingEntry.children || [], transformedProfit.children);
+            }
+        });
+    });
+
+    // 确保每个层级的数据都有完整的年份字段
+    const fillMissingYears = (nodes: any[], computeDates: string[]) => {
+        nodes.forEach((node) => {
+            // 根据 dataType 处理不同类型的数据
+            const formatValue = (value: number) => {
+                if (value == null) return null;
+                
+                if (node.dataType === 2) {
+                    // 百分比类型
+                    return `${((value * 100).toFixed(node.decimalPlace || 2))}%`;
+                } else {
+                    // 数值类型 (dataType === 1 或未定义)
+                    return formatMoneyNumber(value, {
+                        decimalPlaces: node.decimalPlace,
+                        useThousandSeparator: Boolean(node.permil)
+                    });
+                }
+            };
+            
+            computeDates.forEach((date) => {
+                if (!node.hasOwnProperty(`${date}_amount`)) {
+                    node[`${date}_amount`] = formatValue(node.amount);
+                }
+                if (!node.hasOwnProperty(`${date}_budgetAmount`)) {
+                    node[`${date}_budgetAmount`] = formatValue(node.budgetAmount);
+                }
+                if (!node.hasOwnProperty(`${date}_samePeriodAmount`)) {
+                    node[`${date}_samePeriodAmount`] = formatValue(node.samePeriodAmount);
+                }
+                if (!node.hasOwnProperty(`${date}_completionRate`)) {
+                    node[`${date}_completionRate`] = node.completionRate != null ? 
+                        `${((node.completionRate * 100).toFixed(node.decimalPlace || 2))}%` : null;
+                }
+                if (!node.hasOwnProperty(`${date}_yoyRate`)) {
+                    node[`${date}_yoyRate`] = node.yoyRate != null ? 
+                        `${((node.yoyRate * 100).toFixed(node.decimalPlace || 2))}%` : null;
+                }
+                if (!node.hasOwnProperty(`${date}_allowDrillDown`)) {
+                    node[`${date}_allowDrillDown`] = node.allowDrillDown;
+                }
+            });
+
+            if (node.children && Array.isArray(node.children)) {
+                fillMissingYears(node.children, computeDates);
+                if (node.children.length === 0) {
+                    delete node.children;
+                }
+            }
+        });
+    };
+
+    const computeDates = data.map((item) => item.computeDate);
+    const transformedData = Array.from(resultMap.values());
+    fillMissingYears(transformedData, computeDates);
+
+    transformedData.forEach((node) => {
+        if (node.children && node.children.length === 0) {
+            delete node.children;
+        }
+    });
+
+    return transformedData;
+};
+
 
 const getNextUnexpandedKeys = (data: any[], expandedKeys: any[] = []) => {
     let keys: any[] = [];
@@ -242,12 +464,29 @@ const getNextUnexpandedKeys = (data: any[], expandedKeys: any[] = []) => {
 
 export default function DepartmentCostCalc() {
 
+    const location = useLocation();
+    // 从路由路径判断模式,而不是从查询参数
+    const isYearMode = location.pathname.includes('/year');
+    const pageMode = isYearMode ? 'year' : 'month';
+
     const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>({ reportType: 0 });
     const tableRef = useRef<ActionType>();
     const [tabs, set_tabs] = useState<any[]>([]);
 
     const { initialState, setInitialState } = useModel('@@initialState');
-    const [computeRangeDate, set_computeRangeDate] = useState<string[]>(initialState ? [initialState.computeDate, initialState.computeDate] : []);
+    const [computeRangeDate, set_computeRangeDate] = useState<string[]>(() => {
+        if (initialState) {
+            if (isYearMode) {
+                // 年度模式:使用年份格式
+                const year = initialState.computeDate ? initialState.computeDate.substring(0, 4) : '';
+                return [year, year];
+            } else {
+                // 月度模式:使用原有格式
+                return [initialState.computeDate, initialState.computeDate];
+            }
+        }
+        return [];
+    });
     const [responsibleCenters, set_responsibleCenters] = useState<any[]>([]);
     const [currentTabKey, set_currentTabKey] = useState<any | undefined>(undefined);
     const [currentTab, set_currentTab] = useState<any | undefined>(undefined);
@@ -275,68 +514,300 @@ export default function DepartmentCostCalc() {
             },
         ];
 
-        let ifShowPercent = true;
+        if (isYearMode) {
+            // 年度模式的列配置
+            // 获取是否显示预算金额和完成率的参数
+            let showBudgetAndCompletion = false;
+            const { systemId } = JSON.parse(localStorage.getItem('currentSelectedTab') as string)
+            const budgetResp = await getParamsDataBySysId(systemId, '1942871277190647808');
+            if (budgetResp) {
+                showBudgetAndCompletion = budgetResp.value === '1';
+            }
 
-        const { systemId } = JSON.parse(localStorage.getItem('currentSelectedTab') as string)
-        const resp = await getParamsDataBySysId(systemId, '1851077044079824896');
-        if (resp) {
-            ifShowPercent = resp.value == '1' ? true : false;
-        }
+            const yearColumns = data.map((item) => {
+                const year = item.computeDate;
+                const baseYearColumns = [
+                    {
+                        title: '实际',
+                        dataIndex: `${year}_amount`,
+                        key: `${year}_amount`,
+                        width: 100,
+                        align: 'right',
+                        renderText(num: number, record: any) {
+                            const { calcType } = record;
+                            if (calcType == 0) {
+                                return <React.Fragment></React.Fragment>
+                            } else {
+                                // 使用按年份字段化的allowDrillDown
+                                const yearAllowDrillDown = record[`${year}_allowDrillDown`];
+                                // 检查allowDrillDown字段,如果为false则不可下钻
+                                if ((calcType == 1 || calcType == 2 || calcType == 5) && yearAllowDrillDown) {
+                                    return <span onClick={() => { set_nextLevel({ ...record, date: year }); tableRef.current?.reload() }} style={{ color: '#3377FF', cursor: 'pointer' }}>{num}</span>;
+                                } else {
+                                    return num;
+                                }
+                            }
+                        },
+                    },
+                    {
+                        title: '同期',
+                        dataIndex: `${year}_samePeriodAmount`,
+                        key: `${year}_samePeriodAmount`,
+                        width: 100,
+                        align: 'right',
+                        renderText(num: number, record: any) {
+                            const { calcType } = record;
+                            if (calcType == 0) {
+                                return <React.Fragment></React.Fragment>
+                            } else {
+                                return num
+                            }
+                        },
+                    },
+                    {
+                        title: '同比',
+                        dataIndex: `${year}_yoyRate`,
+                        key: `${year}_yoyRate`,
+                        width: 100,
+                        align: 'right',
+                        renderText(text: string, record: any) {
+                            const { calcType } = record;
+                            if (calcType == 0) {
+                                return <React.Fragment></React.Fragment>
+                            } else {
+                                return text
+                            }
+                        },
+                    },
+                ];
+
+                // 根据参数决定是否显示预算和完成率列
+                const conditionalColumns = showBudgetAndCompletion ? [
+                    {
+                        title: '预算',
+                        dataIndex: `${year}_budgetAmount`,
+                        key: `${year}_budgetAmount`,
+                        width: 100,
+                        align: 'right',
+                        renderText(num: number, record: any) {
+                            const { calcType } = record;
+                            if (calcType == 0) {
+                                return <React.Fragment></React.Fragment>
+                            } else {
+                                // 年度模式下不可点击
+                                return num;
+                            }
+                        },
+                    },
+                    {
+                        title: '完成率',
+                        dataIndex: `${year}_completionRate`,
+                        key: `${year}_completionRate`,
+                        width: 100,
+                        align: 'right',
+                        renderText(text: string, record: any) {
+                            const { calcType } = record;
+                            if (calcType == 0) {
+                                return <React.Fragment></React.Fragment>
+                            } else {
+                                return text
+                            }
+                        },
+                    },
+                ] : [];
+
+                return {
+                    title: `${year}年`,
+                    children: [
+                        ...conditionalColumns.slice(0, 1), // 预算列(如果显示)
+                        ...baseYearColumns.slice(0, 1), // 实际列
+                        ...baseYearColumns.slice(1, 2), // 同期列
+                        ...conditionalColumns.slice(1, 2), // 完成率列(如果显示)
+                        ...baseYearColumns.slice(2, 3), // 同比列
+                    ]
+                };
+            });
+            set_columns([...baseColumns, ...yearColumns]);
+        } else {
+            // 月度模式的列配置(原有逻辑)
+            let ifShowPercent = true;
+            let showBudgetAndCompletion = false;
 
+            const { systemId } = JSON.parse(localStorage.getItem('currentSelectedTab') as string)
+            const resp = await getParamsDataBySysId(systemId, '1851077044079824896');
+            if (resp) {
+                ifShowPercent = resp.value == '1' ? true : false;
+            }
 
-        // 获取所有月份并生成列
-        const monthColumns = data.map((item) => {
-            const month = item.computeDate;
-            const base = [
-                {
-                    title: '金额',
+            // 获取是否显示预算金额和完成率的参数
+            const budgetResp = await getParamsDataBySysId(systemId, '1942871277190647808');
+            if (budgetResp) {
+                showBudgetAndCompletion = budgetResp.value === '1';
+            }
+
+            // 获取所有月份并生成列
+            const monthColumns = data.map((item) => {
+                const month = item.computeDate;
+
+                // 构建最终的列数组
+                let finalColumns: any[] = [];
+                
+                // 根据参数决定是否显示预算列(放在实际列前面)
+                if (showBudgetAndCompletion) {
+                    finalColumns.push({
+                        title: '预算',
+                        dataIndex: `${month}_budgetAmount`,
+                        key: `${month}_budgetAmount`,
+                        width: 100,
+                        align: 'right',
+                        renderText(num: number, record: any) {
+                            const { calcType } = record;
+                            if (calcType == 0) {
+                                return <React.Fragment></React.Fragment>
+                            } else {
+                                return num
+                            }
+                        },
+                    });
+                }
+
+                // 添加实际列
+                finalColumns.push({
+                    title: '实际',
                     dataIndex: `${month}_amount`,
                     key: `${month}_amount`,
                     width: 100,
                     align: 'right',
                     renderText(num: number, record: any) {
-
                         const { calcType } = record;
                         if (calcType == 0) {
                             return <React.Fragment></React.Fragment>
                         } else {
-                            if (calcType == 1 || calcType == 2 || calcType == 5) {
-                                return <span onClick={() => { set_nextLevel({ ...record, date: month }); tableRef.current?.reload() }} style={{ color: (calcType == 1 || calcType == 2 || calcType == 5) ? '#3377FF' : '#17181A', cursor: (calcType == 1 || calcType == 2 || calcType == 5) ? 'pointer' : 'default' }}>{num}</span>;
+                            // 使用按月份字段化的allowDrillDown
+                            const monthAllowDrillDown = record[`${month}_allowDrillDown`];
+                            // 保留原有的calcType判断,同时使用allowDrillDown字段控制当前月是否可下钻
+                            if ((calcType == 1 || calcType == 2 || calcType == 5) && monthAllowDrillDown) {
+                                return <span onClick={() => { set_nextLevel({ ...record, date: month }); tableRef.current?.reload() }} style={{ color: '#3377FF', cursor: 'pointer' }}>{num}</span>;
                             } else {
                                 return num
                             }
+                        }
+                    },
+                });
 
-                            //return formatMoneyNumber(num);
+                // 始终显示上期列
+                finalColumns.push({
+                    title: '上期',
+                    dataIndex: `${month}_prevPeriodAmount`,
+                    key: `${month}_prevPeriodAmount`,
+                    width: 100,
+                    align: 'right',
+                    renderText(num: number, record: any) {
+                        const { calcType } = record;
+                        if (calcType == 0) {
+                            return <React.Fragment></React.Fragment>
+                        } else {
+                            return num
                         }
                     },
-                },
-            ]
-            return {
-                title: month,
-                children: ifShowPercent ? [
-                    ...base,
-                    {
+                });
+
+                // 始终显示同期列
+                finalColumns.push({
+                    title: '同期',
+                    dataIndex: `${month}_samePeriodAmount`,
+                    key: `${month}_samePeriodAmount`,
+                    width: 100,
+                    align: 'right',
+                    renderText(num: number, record: any) {
+                        const { calcType } = record;
+                        if (calcType == 0) {
+                            return <React.Fragment></React.Fragment>
+                        } else {
+                            return num
+                        }
+                    },
+                });
+
+                // 根据参数决定是否显示完成率列
+                if (showBudgetAndCompletion) {
+                    finalColumns.push({
+                        title: '完成率',
+                        dataIndex: `${month}_completionRate`,
+                        key: `${month}_completionRate`,
+                        width: 100,
+                        align: 'right',
+                        renderText(text: any, record: any) {
+                            const { calcType } = record;
+                            if (calcType == 0) {
+                                return <React.Fragment></React.Fragment>
+                            } else {
+                                return text
+                            }
+                        },
+                    });
+                }
+
+                // 始终显示环比列
+                finalColumns.push({
+                    title: '环比增长',
+                    dataIndex: `${month}_momRate`,
+                    key: `${month}_momRate`,
+                    width: 100,
+                    align: 'right',
+                    renderText(text: any, record: any) {
+                        const { calcType } = record;
+                        if (calcType == 0) {
+                            return <React.Fragment></React.Fragment>
+                        } else {
+                            return text
+                        }
+                    },
+                });
+
+                // 始终显示同比列
+                finalColumns.push({
+                    title: '同比增长',
+                    dataIndex: `${month}_yoyRate`,
+                    key: `${month}_yoyRate`,
+                    width: 100,
+                    align: 'right',
+                    renderText(text: any, record: any) {
+                        const { calcType } = record;
+                        if (calcType == 0) {
+                            return <React.Fragment></React.Fragment>
+                        } else {
+                            return text
+                        }
+                    },
+                });
+
+                // 如果显示占比,添加占比列
+                if (ifShowPercent) {
+                    finalColumns.push({
                         title: '占比',
                         dataIndex: `${month}_percent`,
                         key: `${month}_percent`,
                         width: 100,
                         align: 'right',
                         renderText(text: number, record: any) {
-
                             const { calcType } = record;
                             if (calcType == 0) {
                                 return <React.Fragment></React.Fragment>
                             } else {
                                 return text
                             }
-
                         },
-                    },
-                ] : [...base]
-            };
-        });
-        set_columns([...baseColumns, ...monthColumns]);
+                    });
+                }
 
+                return {
+                    title: month,
+                    children: finalColumns
+                };
+            });
+            set_columns([...baseColumns, ...monthColumns]);
+        }
     };
 
 
@@ -354,7 +825,15 @@ export default function DepartmentCostCalc() {
         if (!responsibilityCode) return []
 
         if (!nextLevel) {
-            const resp = await getReportProjectSettingList({ ...params });
+            let resp;
+            if (isYearMode) {
+                // 年度模式调用年度接口
+                resp = await getBatchComputeProfitListByYear({ ...params });
+            } else {
+                // 月度模式调用原有接口
+                resp = await getReportProjectSettingList({ ...params });
+            }
+            
             if (resp) {
                 // if (filter) {
                 //     const filterData = searchTree(resp, filter);
@@ -367,7 +846,7 @@ export default function DepartmentCostCalc() {
                 //     }
                 // }
 
-                const realData = transformData(resp);
+                const realData = isYearMode ? transformYearData(resp) : transformData(resp);
                 if (currentTab.value == '1') {
                     const allParents = findAllParents(realData);
 
@@ -747,13 +1226,26 @@ export default function DepartmentCostCalc() {
 
     useEffect(() => {
         if (currentSelectedRespon) {
-            set_tableDataFilterParams({
-                ...tableDataFilterParams,
-                responsibilityCode: currentSelectedRespon.responsibilityCode,
-                reportType: currentTabKey,
-                beginComputeDate: computeRangeDate[0],
-                endComputeDate: computeRangeDate[1],
-            });
+            if (isYearMode) {
+                // 年度模式:确保传递的是纯年份
+                const startYear = computeRangeDate[0] ? computeRangeDate[0].substring(0, 4) : '';
+                const endYear = computeRangeDate[1] ? computeRangeDate[1].substring(0, 4) : '';
+                set_tableDataFilterParams({
+                    ...tableDataFilterParams,
+                    responsibilityCode: currentSelectedRespon.responsibilityCode,
+                    reportType: currentTabKey,
+                    startYear: startYear,
+                    endYear: endYear,
+                });
+            } else {
+                set_tableDataFilterParams({
+                    ...tableDataFilterParams,
+                    responsibilityCode: currentSelectedRespon.responsibilityCode,
+                    reportType: currentTabKey,
+                    beginComputeDate: computeRangeDate[0],
+                    endComputeDate: computeRangeDate[1],
+                });
+            }
             set_nextLevel(undefined);
         }
         set_allParentsKeys([]);
@@ -769,23 +1261,37 @@ export default function DepartmentCostCalc() {
         <KCIMPagecontainer className='DepartmentCostCalc' title={false}>
             <div className='header'>
                 <div className="search">
-                    <span>核算年月:</span>
+                    <span>{isYearMode ? '核算年份:' : '核算年月:'}</span>
                     <RangePicker
                         onChange={(data, dateString) => {
                             // console.log({data,dateString});
                             set_computeRangeDate(dateString);
                             setInitialState((s: any) => ({ ...s, computeDate: dateString[0], }))
-                            set_tableDataFilterParams({
-                                ...tableDataFilterParams,
-                                beginComputeDate: dateString[0],
-                                endComputeDate: dateString[1],
-                            });
+                            if (isYearMode) {
+                                // 年度模式:提取年份部分
+                                const startYear = data?.[0]?.year()?.toString() || dateString[0];
+                                const endYear = data?.[1]?.year()?.toString() || dateString[1];
+                                set_tableDataFilterParams({
+                                    ...tableDataFilterParams,
+                                    startYear: startYear,
+                                    endYear: endYear,
+                                });
+                            } else {
+                                set_tableDataFilterParams({
+                                    ...tableDataFilterParams,
+                                    beginComputeDate: dateString[0],
+                                    endComputeDate: dateString[1],
+                                });
+                            }
                         }}
-                        picker="month"
+                        picker={isYearMode ? "year" : "month"}
                         locale={locale}
                         autoComplete="off"
-                        defaultValue={[moment(computeRangeDate[0], 'YYYY-MM'), moment(computeRangeDate[1], 'YYYY-MM')]}
-                        format="YYYY-MM"
+                        defaultValue={isYearMode ? 
+                            [moment(computeRangeDate[0], 'YYYY'), moment(computeRangeDate[1], 'YYYY')] :
+                            [moment(computeRangeDate[0], 'YYYY-MM'), moment(computeRangeDate[1], 'YYYY-MM')]
+                        }
+                        format={isYearMode ? "YYYY" : "YYYY-MM"}
                     />
                 </div>
                 {/* <div className='btnGoup'>

+ 8 - 0
src/pages/departmentCostCheck/service.ts

@@ -20,6 +20,14 @@ export const getReportProjectSettingList = (params:any) => {
   });
 }
 
+//获取年度损益数据
+export const getBatchComputeProfitListByYear = (params:any) => {
+  return request('/costAccount/costdepartmentprofit/getBatchComputeProfitListByYear', {
+    method: 'GET',
+    params:{...params},
+  });
+}
+
 
 //获取下钻表格数据
 export const getNextLevelTableData = (params:any) => {

+ 2 - 2
src/pages/departmentMenzhuCostCalc/index.tsx

@@ -427,11 +427,11 @@ export default function DepartmentCostCalc() {
                                     } else if (num === null || num === undefined) {
                                         return num;
                                     }
-                                    return `${formatMoneyNumber(num, formatOptions)}%`;
+                                    return `${formatMoneyNumber(num, formatOptions)}%`; 
                                 }
                                 return formatMoneyNumber(num, formatOptions);
                             },
-                        },
+                        },  
                         {
                             title: '环比增长率',
                             align:'right',

+ 1031 - 0
src/pages/hospitalProfitAndLoss/index.tsx

@@ -0,0 +1,1031 @@
+/*
+ * @Author: code4eat awesomedema@gmail.com
+ * @Date: 2025-01-20 10:00:00
+ * @LastEditors: code4eat awesomedema@gmail.com
+ * @LastEditTime: 2025-08-01 15:13:42
+ * @FilePath: /CostAccountingSys/src/pages/hospitalProfitAndLoss/index.tsx
+ * @Description: 全院损益报表查询界面
+ */
+
+import KCIMPagecontainer from '@/components/KCIMPageContainer';
+import { KCIMTable } from '@/components/KCIMTable';
+import { createFromIconfontCN } from '@ant-design/icons';
+import { ActionType } from '@ant-design/pro-components';
+import { ProColumns } from '@ant-design/pro-table';
+import { message, Tabs, Input, DatePicker, Alert } from 'antd';
+import { Key, useEffect, useRef, useState } from 'react';
+import * as XLSX from 'xlsx';
+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 { getBatchHospProfitList } from './service';
+import './style.less';
+import React from 'react';
+import { getDicDataBySysId, getParamsDataBySysId } from '@/services/getDic';
+import { formatMoneyNumber } from '@/utils/format';
+import { useModel } from '@umijs/max';
+import { useLocation } from '@umijs/max';
+
+const { RangePicker } = DatePicker;
+
+const IconFont = createFromIconfontCN({
+    scriptUrl: '',
+});
+
+// 数据转换函数
+const transformData = (data: any[]) => {
+    const resultMap: Map<string, { reportName: string; key: string; children?: any[];[key: string]: any }> = new Map();
+
+    // 转换单个条目
+    const transformEntry = (entry: any, computeDate: string, allowDrillDown?: boolean, isMonthlyAccumulation?: boolean, isAuditMonth?: boolean) => {
+        if (!entry) return {};
+        const transformedEntry = { ...entry };
+        
+        // 添加标志字段到每个节点
+        if (allowDrillDown !== undefined) {
+            transformedEntry.allowDrillDown = allowDrillDown;
+        }
+        if (isMonthlyAccumulation !== undefined) {
+            transformedEntry.isMonthlyAccumulation = isMonthlyAccumulation;
+        }
+        if (isAuditMonth !== undefined) {
+            transformedEntry.isAuditMonth = isAuditMonth;
+        }
+        
+        // 根据 dataType 处理不同类型的数据
+        const formatValue = (value: number) => {
+            if (value == null) return null;
+            
+            if (entry.dataType === 2) {
+                // 百分比类型
+                return `${((value * 100).toFixed(entry.decimalPlace || 2))}%`;
+            } else {
+                // 数值类型 (dataType === 1 或未定义)
+                return formatMoneyNumber(value, {
+                    decimalPlaces: entry.decimalPlace,
+                    useThousandSeparator: Boolean(entry.permil)
+                });
+            }
+        };
+        
+        // 处理各个字段
+        transformedEntry[`${computeDate}_amount`] = formatValue(entry.amount);
+        transformedEntry[`${computeDate}_budgetAmount`] = formatValue(entry.budgetAmount);
+        transformedEntry[`${computeDate}_prevPeriodAmount`] = formatValue(entry.prevPeriodAmount);
+        transformedEntry[`${computeDate}_samePeriodAmount`] = formatValue(entry.samePeriodAmount);
+        
+        // 百分比字段始终显示为百分比
+        transformedEntry[`${computeDate}_completionRate`] = entry.completionRate != null ? 
+            `${((entry.completionRate * 100).toFixed(entry.decimalPlace || 2))}%` : null;
+        transformedEntry[`${computeDate}_momRate`] = entry.momRate != null ? 
+            `${((entry.momRate * 100).toFixed(entry.decimalPlace || 2))}%` : null;
+        transformedEntry[`${computeDate}_yoyRate`] = entry.yoyRate != null ? 
+            `${((entry.yoyRate * 100).toFixed(entry.decimalPlace || 2))}%` : null;
+        
+        return transformedEntry;
+    };
+
+    // 合并新的条目到现有条目,不覆盖已有的字段
+    const mergeEntries = (existingEntry: any, newEntry: any) => {
+        for (const key in newEntry) {
+            if (!existingEntry[key] || key.endsWith('_amount') || key.endsWith('_percent')) {
+                existingEntry[key] = newEntry[key];
+            }
+        }
+    };
+
+    // 递归处理每个节点及其子节点
+    const processNode = (node: any, computeDate: string, allowDrillDown?: boolean, isMonthlyAccumulation?: boolean, isAuditMonth?: boolean): any => {
+        if (!node) return {};
+
+        const transformedNode = transformEntry(node, computeDate, allowDrillDown, isMonthlyAccumulation, isAuditMonth);
+
+        if (node.children && Array.isArray(node.children)) {
+            const processedChildren = node.children.map((child: any) => processNode(child, computeDate, allowDrillDown, isMonthlyAccumulation, isAuditMonth));
+            transformedNode.children = mergeChildren(transformedNode.children || [], processedChildren);
+        }
+
+        return transformedNode;
+    };
+
+    // 合并子节点,确保同一个父节点下的子节点是唯一的
+    const mergeChildren = (existingChildren: any[], newChildren: any[]) => {
+        newChildren.forEach((newChild) => {
+            const existingChild = existingChildren.find((child) => child.reportName === newChild.reportName && child.key === newChild.key);
+
+            if (existingChild) {
+                mergeEntries(existingChild, newChild);
+                if (newChild.children && Array.isArray(newChild.children)) {
+                    existingChild.children = mergeChildren(existingChild.children || [], newChild.children);
+                }
+            } else {
+                existingChildren.push(newChild);
+            }
+        });
+
+        return existingChildren;
+    };
+
+    // 主处理逻辑
+    data.forEach((item) => {
+        const { computeDate, profitVoList, allowDrillDown, monthlyAccumulation, auditMonth } = item;
+
+        if (!profitVoList || !Array.isArray(profitVoList)) {
+            return;
+        }
+
+        profitVoList.forEach((profit) => {
+            if (!resultMap.has(profit.reportName)) {
+                resultMap.set(profit.reportName, {
+                    ...transformEntry(profit, computeDate, allowDrillDown, monthlyAccumulation, auditMonth),
+                    key: profit.reportName,
+                });
+            }
+
+            const existingEntry = resultMap.get(profit.reportName)!;
+            const transformedProfit = processNode(profit, computeDate, allowDrillDown, monthlyAccumulation, auditMonth);
+
+            mergeEntries(existingEntry, transformedProfit);
+
+            if (transformedProfit.children && Array.isArray(transformedProfit.children)) {
+                existingEntry.children = mergeChildren(existingEntry.children || [], transformedProfit.children);
+            }
+        });
+    });
+
+    // 确保每个层级的数据都有完整的月份字段
+    const fillMissingMonths = (nodes: any[], computeDates: string[]) => {
+        nodes.forEach((node) => {
+            computeDates.forEach((date) => {
+                if (!node.hasOwnProperty(`${date}_amount`)) {
+                    node[`${date}_amount`] = node.amount != null ? formatMoneyNumber(node.amount,{decimalPlaces:node.decimalPlace,useThousandSeparator:Boolean(node.permil)}) : null;
+                }
+                if (!node.hasOwnProperty(`${date}_budgetAmount`)) {
+                    node[`${date}_budgetAmount`] = node.budgetAmount != null ? formatMoneyNumber(node.budgetAmount,{decimalPlaces:node.decimalPlace,useThousandSeparator:Boolean(node.permil)}) : null;
+                }
+                if (!node.hasOwnProperty(`${date}_prevPeriodAmount`)) {
+                    node[`${date}_prevPeriodAmount`] = node.prevPeriodAmount != null ? formatMoneyNumber(node.prevPeriodAmount,{decimalPlaces:node.decimalPlace,useThousandSeparator:Boolean(node.permil)}) : null;
+                }
+                if (!node.hasOwnProperty(`${date}_samePeriodAmount`)) {
+                    node[`${date}_samePeriodAmount`] = node.samePeriodAmount != null ? formatMoneyNumber(node.samePeriodAmount,{decimalPlaces:node.decimalPlace,useThousandSeparator:Boolean(node.permil)}) : null;
+                }
+                if (!node.hasOwnProperty(`${date}_completionRate`)) {
+                    node[`${date}_completionRate`] = node.completionRate != null ? `${((node.completionRate * 100).toFixed(node.decimalPlace || 2))}%` : null;
+                }
+                if (!node.hasOwnProperty(`${date}_momRate`)) {
+                    node[`${date}_momRate`] = node.momRate != null ? `${((node.momRate * 100).toFixed(node.decimalPlace || 2))}%` : null;
+                }
+                if (!node.hasOwnProperty(`${date}_yoyRate`)) {
+                    node[`${date}_yoyRate`] = node.yoyRate != null ? `${((node.yoyRate * 100).toFixed(node.decimalPlace || 2))}%` : null;
+                }
+            });
+
+            if (node.children && Array.isArray(node.children)) {
+                fillMissingMonths(node.children, computeDates);
+                if (node.children.length === 0) {
+                    delete node.children;
+                }
+            }
+        });
+    };
+
+    const computeDates = data.map((item) => item.computeDate);
+    const transformedData = Array.from(resultMap.values());
+    fillMissingMonths(transformedData, computeDates);
+
+    transformedData.forEach((node) => {
+        if (node.children && node.children.length === 0) {
+            delete node.children;
+        }
+    });
+
+    return transformedData;
+};
+
+export default function HospitalProfitAndLoss() {
+    const location = useLocation();
+    // 从路由路径判断模式
+    const isYearMode = location.pathname.includes('/year');
+    const pageMode = isYearMode ? 'year' : 'month';
+
+    const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>({ reportType: 0 });
+    const tableRef = useRef<ActionType>();
+    const [tabs, set_tabs] = useState<any[]>([]);
+
+    const { initialState, setInitialState } = useModel('@@initialState');
+    const [computeRangeDate, set_computeRangeDate] = useState<string[]>(() => {
+        if (initialState) {
+            if (isYearMode) {
+                // 年度模式:使用年份格式
+                const year = initialState.computeDate ? initialState.computeDate.substring(0, 4) : '';
+                return [year, year];
+            } else {
+                // 月度模式:使用原有格式
+                return [initialState.computeDate, initialState.computeDate];
+            }
+        }
+        return [];
+    });
+    
+    const [currentTabKey, set_currentTabKey] = useState<any | undefined>(undefined);
+    const [currentTab, set_currentTab] = useState<any | undefined>(undefined);
+    const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState('');
+    const [allParentsKeys, set_allParentsKeys] = useState<Key[]>([]);
+    const [dataSource, set_dataSource] = useState<any[]>([]);
+    const [columns, set_columns] = useState<any[]>([]);
+    const [showBudgetAndCompletion, set_showBudgetAndCompletion] = useState(false);
+
+    // 动态生成列定义函数
+    const generateDataColumns = async (data: any[]) => {
+        const baseColumns = [
+            {
+                title: '报表项目名称',
+                dataIndex: 'reportName',
+                key: 'reportName',
+                width: 200,
+                fixed: 'left'
+            },
+        ];
+
+        if (isYearMode) {
+            // 年度模式的列配置
+            const yearColumns = data.map((item) => {
+                const year = item.computeDate;
+                const isAuditMonth = item.auditMonth;
+                
+                // 如果是审计月,只显示实际金额列
+                if (isAuditMonth) {
+                    return {
+                        title: `${year}年(审计月)`,
+                        children: [
+                            {
+                                title: '实际',
+                                dataIndex: `${year}_amount`,
+                                key: `${year}_amount`,
+                                width: 150,
+                                align: 'right',
+                                renderText(num: number, record: any) {
+                                    const { calcType } = record;
+                                    if (calcType == 0) {
+                                        return <React.Fragment></React.Fragment>
+                                    } else {
+                                        return num;
+                                    }
+                                },
+                            }
+                        ]
+                    };
+                } else {
+                    // 非审计月的正常列配置
+                    const baseYearColumns = [
+                        {
+                            title: '实际',
+                            dataIndex: `${year}_amount`,
+                            key: `${year}_amount`,
+                            width: 100,
+                            align: 'right',
+                            renderText(num: number, record: any) {
+                                const { calcType } = record;
+                                if (calcType == 0) {
+                                    return <React.Fragment></React.Fragment>
+                                } else {
+                                    return num;
+                                }
+                            },
+                        },
+                        {
+                            title: '同期',
+                            dataIndex: `${year}_samePeriodAmount`,
+                            key: `${year}_samePeriodAmount`,
+                            width: 100,
+                            align: 'right',
+                            renderText(num: number, record: any) {
+                                const { calcType } = record;
+                                if (calcType == 0) {
+                                    return <React.Fragment></React.Fragment>
+                                } else {
+                                    return num
+                                }
+                            },
+                        },
+                        {
+                            title: '同比',
+                            dataIndex: `${year}_yoyRate`,
+                            key: `${year}_yoyRate`,
+                            width: 100,
+                            align: 'right',
+                            renderText(text: string, record: any) {
+                                const { calcType } = record;
+                                if (calcType == 0) {
+                                    return <React.Fragment></React.Fragment>
+                                } else {
+                                    return text
+                                }
+                            },
+                        },
+                    ];
+
+                    // 根据参数决定是否显示预算和完成率列
+                    const conditionalColumns = showBudgetAndCompletion ? [
+                        {
+                            title: '预算',
+                            dataIndex: `${year}_budgetAmount`,
+                            key: `${year}_budgetAmount`,
+                            width: 100,
+                            align: 'right',
+                            renderText(num: number, record: any) {
+                                const { calcType } = record;
+                                if (calcType == 0) {
+                                    return <React.Fragment></React.Fragment>
+                                } else {
+                                    return num;
+                                }
+                            },
+                        },
+                        {
+                            title: '完成率',
+                            dataIndex: `${year}_completionRate`,
+                            key: `${year}_completionRate`,
+                            width: 100,
+                            align: 'right',
+                            renderText(text: string, record: any) {
+                                const { calcType } = record;
+                                if (calcType == 0) {
+                                    return <React.Fragment></React.Fragment>
+                                } else {
+                                    return text
+                                }
+                            },
+                        },
+                    ] : [];
+
+                    return {
+                        title: `${year}年`,
+                        children: [
+                            ...conditionalColumns.slice(0, 1), // 预算列(如果显示)
+                            ...baseYearColumns.slice(0, 1), // 实际列
+                            ...baseYearColumns.slice(1, 2), // 同期列
+                            ...conditionalColumns.slice(1, 2), // 完成率列(如果显示)
+                            ...baseYearColumns.slice(2, 3), // 同比列
+                        ]
+                    };
+                }
+            });
+            set_columns([...baseColumns, ...yearColumns]);
+        } else {
+            // 月度模式的列配置
+            const monthColumns = data.map((item) => {
+                const month = item.computeDate;
+                const isMonthlyAccumulation = item.monthlyAccumulation;
+                const isAuditMonth = item.auditMonth;
+
+                // 构建最终的列数组
+                let finalColumns: any[] = [];
+                
+                // 如果是审计月,只显示实际金额列
+                if (isAuditMonth) {
+                    finalColumns.push({
+                        title: '实际',
+                        dataIndex: `${month}_amount`,
+                        key: `${month}_amount`,
+                        width: 150,
+                        align: 'right',
+                        renderText(num: number, record: any) {
+                            const { calcType } = record;
+                            if (calcType == 0) {
+                                return <React.Fragment></React.Fragment>
+                            } else {
+                                return num
+                            }
+                        },
+                    });
+                } else {
+                    // 非审计月的正常列配置
+                    // 根据参数决定是否显示预算列(放在实际列前面)
+                    if (showBudgetAndCompletion) {
+                        finalColumns.push({
+                            title: '预算',
+                            dataIndex: `${month}_budgetAmount`,
+                            key: `${month}_budgetAmount`,
+                            width: 100,
+                            align: 'right',
+                            renderText(num: number, record: any) {
+                                const { calcType } = record;
+                                if (calcType == 0) {
+                                    return <React.Fragment></React.Fragment>
+                                } else {
+                                    return num
+                                }
+                            },
+                        });
+                    }
+
+                    // 添加实际列
+                    finalColumns.push({
+                        title: '实际',
+                        dataIndex: `${month}_amount`,
+                        key: `${month}_amount`,
+                        width: 100,
+                        align: 'right',
+                        renderText(num: number, record: any) {
+                            const { calcType } = record;
+                            if (calcType == 0) {
+                                return <React.Fragment></React.Fragment>
+                            } else {
+                                return num
+                            }
+                        },
+                    });
+
+                    // 始终显示上期列
+                    finalColumns.push({
+                        title: '上期',
+                        dataIndex: `${month}_prevPeriodAmount`,
+                        key: `${month}_prevPeriodAmount`,
+                        width: 100,
+                        align: 'right',
+                        renderText(num: number, record: any) {
+                            const { calcType } = record;
+                            if (calcType == 0) {
+                                return <React.Fragment></React.Fragment>
+                            } else {
+                                return num
+                            }
+                        },
+                    });
+
+                    // 始终显示同期列
+                    finalColumns.push({
+                        title: '同期',
+                        dataIndex: `${month}_samePeriodAmount`,
+                        key: `${month}_samePeriodAmount`,
+                        width: 100,
+                        align: 'right',
+                        renderText(num: number, record: any) {
+                            const { calcType } = record;
+                            if (calcType == 0) {
+                                return <React.Fragment></React.Fragment>
+                            } else {
+                                return num
+                            }
+                        },
+                    });
+
+                    // 根据参数决定是否显示完成率列
+                    if (showBudgetAndCompletion) {
+                        finalColumns.push({
+                            title: '完成率',
+                            dataIndex: `${month}_completionRate`,
+                            key: `${month}_completionRate`,
+                            width: 100,
+                            align: 'right',
+                            renderText(text: any, record: any) {
+                                const { calcType } = record;
+                                if (calcType == 0) {
+                                    return <React.Fragment></React.Fragment>
+                                } else {
+                                    return text
+                                }
+                            },
+                        });
+                    }
+
+                    // 始终显示环比列
+                    finalColumns.push({
+                        title: '环比增长',
+                        dataIndex: `${month}_momRate`,
+                        key: `${month}_momRate`,
+                        width: 100,
+                        align: 'right',
+                        renderText(text: any, record: any) {
+                            const { calcType } = record;
+                            if (calcType == 0) {
+                                return <React.Fragment></React.Fragment>
+                            } else {
+                                return text
+                            }
+                        },
+                    });
+
+                    // 始终显示同比列
+                    finalColumns.push({
+                        title: '同比增长',
+                        dataIndex: `${month}_yoyRate`,
+                        key: `${month}_yoyRate`,
+                        width: 100,
+                        align: 'right',
+                        renderText(text: any, record: any) {
+                            const { calcType } = record;
+                            if (calcType == 0) {
+                                return <React.Fragment></React.Fragment>
+                            } else {
+                                return text
+                            }
+                        },
+                    });
+                }
+
+                // 构建月份标题,包含特殊标志
+                let monthTitle = month;
+                if (isMonthlyAccumulation) {
+                    monthTitle += '(月累计)';
+                }
+                if (isAuditMonth) {
+                    monthTitle += '(审计月)';
+                }
+
+                return {
+                    title: monthTitle,
+                    children: finalColumns
+                };
+            });
+            set_columns([...baseColumns, ...monthColumns]);
+        }
+    };
+
+    // 前端检索函数
+    const filterDataByKeyword = (data: any[], keyword: string): any[] => {
+        if (!keyword || keyword.trim() === '') {
+            return data;
+        }
+
+        const filterNode = (node: any): any | null => {
+            // 检查当前节点是否匹配
+            const isMatch = node.reportName && 
+                node.reportName.toLowerCase().includes(keyword.toLowerCase());
+
+            // 递归过滤子节点
+            let filteredChildren: any[] = [];
+            if (node.children && Array.isArray(node.children)) {
+                filteredChildren = node.children
+                    .map(filterNode)
+                    .filter((child: any) => child !== null);
+            }
+
+            // 如果当前节点匹配或有匹配的子节点,则保留该节点
+            if (isMatch || filteredChildren.length > 0) {
+                return {
+                    ...node,
+                    children: filteredChildren.length > 0 ? filteredChildren : undefined
+                };
+            }
+
+            return null;
+        };
+
+        return data
+            .map(filterNode)
+            .filter((node: any) => node !== null);
+    };
+
+    const getTableData = async (params: any) => {
+        const { beginComputeDate, endComputeDate, reportType, filter } = params;
+        if (!beginComputeDate || !endComputeDate || reportType === undefined) {
+            // 参数不完整时,清空数据并返回空结果
+            set_dataSource([]);
+            set_columns([{
+                title: '报表项目名称',
+                dataIndex: 'reportName',
+                key: 'reportName',
+                width: 200,
+                fixed: 'left'
+            }]);
+            return {
+                data: [],
+                success: true,
+            }
+        }
+
+        try {
+            const resp = await getBatchHospProfitList({ 
+                beginComputeDate, 
+                endComputeDate, 
+                reportType
+            });
+            
+            if (resp) {
+                const realData = transformData(resp);
+                // 应用前端检索过滤
+                const filteredData = filterDataByKeyword(realData, filter || '');
+                generateDataColumns(resp);
+                set_dataSource(filteredData);
+                return {
+                    data: filteredData,
+                    success: true,
+                }
+            } else {
+                // 请求成功但没有数据
+                generateDataColumns([]);
+                set_dataSource([]);
+                return {
+                    data: [],
+                    success: true,
+                }
+            }
+        } catch (error) {
+            // 请求失败时,清空数据并返回错误状态
+            console.error('获取数据失败:', error);
+            set_dataSource([]);
+            set_columns([{
+                title: '报表项目名称',
+                dataIndex: 'reportName',
+                key: 'reportName',
+                width: 200,
+                fixed: 'left'
+            }]);
+            return {
+                data: [],
+                success: false,
+                error: error
+            }
+        }
+    };
+
+    const onTabChanged = (key: Key) => {
+        set_currentTabKey(key);
+        const needItem = tabs.filter((a) => a.key == key);
+        if (needItem.length > 0) set_currentTab(needItem[0]);
+        // 切换tab时立即清空数据源,避免显示上一个tab的数据
+        set_dataSource([]);
+        // 同时清空列定义,避免显示错误的列结构
+        set_columns([{
+            title: '报表项目名称',
+            dataIndex: 'reportName',
+            key: 'reportName',
+            width: 200,
+            fixed: 'left'
+        }]);
+    };
+
+    const getTabs = async () => {
+        const { systemId } = JSON.parse((localStorage.getItem('currentSelectedTab')) as string)
+        const resp = await getDicDataBySysId(systemId, 'PROFIT_REPORT_TYPE');
+        if (resp) {
+            const { dataVoList } = resp;
+            // 过滤只显示value字段等于2的字典项
+            const filteredDataVoList = dataVoList.filter((a: any) => a.value === '2');
+            const tempArr = filteredDataVoList.map((a: any) => ({ label: a.name, key: Number(a.code), value: a.value }));
+            set_tabs([...tempArr]);
+            set_currentTabKey(tempArr[0].key);
+            set_currentTab(tempArr[0]);
+        }
+    };
+
+    const getShowBudgetAndCompletion = async () => {
+        const { systemId } = JSON.parse(localStorage.getItem('currentSelectedTab') as string)
+        const resp = await getParamsDataBySysId(systemId, '1942871277190647808');
+        if (resp) {
+            set_showBudgetAndCompletion(resp.value === '1');
+        }
+    };
+
+    const tableDataSearchHandle = (paramName: string) => {
+        set_tableDataFilterParams({
+            ...tableDataFilterParams,
+            [`${paramName}`]: tableDataSearchKeywords
+        })
+    };
+
+    const getHeaderRows = (columns: any[], level = 0, headerRows: any[] = [], maxLevel = 0) => {
+        headerRows[level] = headerRows[level] || [];
+        columns.forEach((col: { title: any; children: any; }) => {
+            const colSpan = getColSpan(col);
+            headerRows[level].push({ title: col.title, colSpan, rowSpan: col.children ? 1 : maxLevel - level });
+            if (col.children) {
+                getHeaderRows(col.children, level + 1, headerRows, maxLevel);
+            } else {
+                // 填充空白单元格
+                for (let i = level + 1; i < maxLevel; i++) {
+                    headerRows[i] = headerRows[i] || [];
+                    headerRows[i].push({ title: '', colSpan: 1, rowSpan: 1 });
+                }
+            }
+        });
+        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);
+            }
+        });
+        return leafColumns;
+    };
+
+    const addRowWithIndentation = (record: any, level: number, leafColumns: any[], worksheetData: any[]) => {
+        const row = leafColumns.map(col => record[col.dataIndex] ?? '');
+        row[0] = ' '.repeat(level * 4) + row[0]; // 在第一列前添加缩进空格以表示层级
+        worksheetData.push(row);
+        if (record.children) {
+            record.children.forEach((child: any) => addRowWithIndentation(child, level + 1, leafColumns, worksheetData));
+        }
+    };
+
+    const handleExport = () => {
+        try {
+            const workbook = XLSX.utils.book_new();
+            const worksheetData: any[] = [];
+
+            // 获取最大层级
+            const maxLevel = columns.reduce((max, col) => Math.max(max, getMaxLevel(col)), 0);
+
+            // 生成多层级表头
+            const headerRows = getHeaderRows(columns, 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(columns);
+
+            // 添加数据并处理树结构
+            dataSource.forEach(record => addRowWithIndentation(record, 0, leafColumns, worksheetData));
+
+            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' }
+                    };
+                }
+            });
+
+            XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
+
+            const wbout = XLSX.write(workbook, { bookType: 'xlsx', type: 'binary' });
+            const s2ab = (s: string) => {
+                const buf = new ArrayBuffer(s.length);
+                const view = new Uint8Array(buf);
+                for (let i = 0; i < s.length; i++) view[i] = s.charCodeAt(i) & 0xFF;
+                return buf;
+            };
+
+            saveAs(new Blob([s2ab(wbout)], { type: 'application/octet-stream' }), currentTab ? `${currentTab.label}.xlsx` : '全院损益报表.xlsx');
+        } catch (error) {
+            console.error('导出失败:', error);
+            message.error('导出失败');
+        }
+    };
+
+    const handleExpandNext = () => {
+        // 当前所有未展开的节点,第一层优先展开
+        const keysToExpand = getNextUnexpandedKeys(dataSource, allParentsKeys);
+        set_allParentsKeys((prev) => Array.from(new Set([...prev, ...keysToExpand])));
+    };
+
+    const handleCollapseAll = () => {
+        set_allParentsKeys([]);
+    };
+
+    // 根据ID查找节点
+    const findNodeById = (data: any[], nodeId: any): any => {
+        for (const node of data) {
+            if (node.reportId === nodeId) {
+                return node;
+            }
+            if (node.children) {
+                const found = findNodeById(node.children, nodeId);
+                if (found) return found;
+            }
+        }
+        return null;
+    };
+
+    // 获取节点的所有子节点ID
+    const getAllChildrenIds = (node: any): any[] => {
+        let ids: any[] = [];
+        if (node.children) {
+            node.children.forEach((child: any) => {
+                ids.push(child.reportId);
+                ids = ids.concat(getAllChildrenIds(child));
+            });
+        }
+        return ids;
+    };
+
+    const getNextUnexpandedKeys = (data: any[], expandedKeys: any[] = []) => {
+        let keys: any[] = [];
+
+        const traverse = (nodes: any) => {
+            for (const node of nodes) {
+                // 如果当前节点还没有展开,就把它加入 keys
+                if (!expandedKeys.includes(node.reportId)) {
+                    keys.push(node.reportId);
+                }
+
+                // 如果当前节点已经展开,继续遍历子节点
+                if (node.children && expandedKeys.includes(node.reportId)) {
+                    traverse(node.children);
+                }
+            }
+        };
+
+        traverse(data);
+
+        return keys;
+    };
+
+    useEffect(() => {
+        if (computeRangeDate && currentTabKey != undefined) {
+            set_allParentsKeys([]);
+        }
+    }, [computeRangeDate, currentTabKey]);
+
+    useEffect(() => {
+        if (currentTabKey !== undefined) {
+            if (isYearMode) {
+                // 年度模式:确保传递的是纯年份
+                const startYear = computeRangeDate[0] ? computeRangeDate[0].substring(0, 4) : '';
+                const endYear = computeRangeDate[1] ? computeRangeDate[1].substring(0, 4) : '';
+                set_tableDataFilterParams({
+                    ...tableDataFilterParams,
+                    reportType: currentTabKey,
+                    beginComputeDate: startYear,
+                    endComputeDate: endYear,
+                });
+            } else {
+                set_tableDataFilterParams({
+                    ...tableDataFilterParams,
+                    reportType: currentTabKey,
+                    beginComputeDate: computeRangeDate[0],
+                    endComputeDate: computeRangeDate[1],
+                });
+            }
+        }
+        set_allParentsKeys([]);
+    }, [currentTabKey, computeRangeDate]);
+
+    useEffect(() => {
+        getTabs();
+        getShowBudgetAndCompletion();
+    }, []);
+
+    return (
+        <KCIMPagecontainer className='HospitalProfitAndLoss' title={false}>
+            <div className='header'>
+                <div className="search">
+                    <span>{isYearMode ? '核算年份:' : '核算年月:'}</span>
+                    <RangePicker
+                        onChange={(data, dateString) => {
+                            set_computeRangeDate(dateString);
+                            setInitialState((s: any) => ({ ...s, computeDate: dateString[0], }))
+                            if (isYearMode) {
+                                // 年度模式:提取年份部分
+                                const startYear = data?.[0]?.year()?.toString() || dateString[0];
+                                const endYear = data?.[1]?.year()?.toString() || dateString[1];
+                                set_tableDataFilterParams({
+                                    ...tableDataFilterParams,
+                                    beginComputeDate: startYear,
+                                    endComputeDate: endYear,
+                                });
+                            } else {
+                                set_tableDataFilterParams({
+                                    ...tableDataFilterParams,
+                                    beginComputeDate: dateString[0],
+                                    endComputeDate: dateString[1],
+                                });
+                            }
+                        }}
+                        picker={isYearMode ? "year" : "month"}
+                        locale={locale}
+                        autoComplete="off"
+                        defaultValue={isYearMode ? 
+                            [moment(computeRangeDate[0], 'YYYY'), moment(computeRangeDate[1], 'YYYY')] :
+                            [moment(computeRangeDate[0], 'YYYY-MM'), moment(computeRangeDate[1], 'YYYY-MM')]
+                        }
+                        format={isYearMode ? "YYYY" : "YYYY-MM"}
+                    />
+                </div>
+            </div>
+            <div className='content'>
+                <Tabs
+                    defaultActiveKey={tabs.length > 0 ? tabs[0].key : undefined}
+                    items={tabs}
+                    key={'key'}
+                    onChange={(key) => onTabChanged(key)}
+                />
+                <div className='inner'>
+                    <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='calc' onClick={() => handleExport()}>导出</span>
+                            </div>
+                        </div>
+                        
+                        <KCIMTable 
+                            pagination={false}
+                            bordered
+                            rowClassName={(record) => (record.children ? 'has-children hover-row' : 'hover-row')}
+                            expandable={{
+                                defaultExpandAllRows: false, 
+                                expandedRowKeys: allParentsKeys,
+                                onExpand(expanded, record) {
+                                    const { reportId } = record;
+                                    if (!expanded) {
+                                        // 收起时,移除当前节点及其所有子节点
+                                        const removeNodeAndChildren = (nodeId: any, keys: any[]) => {
+                                            const node = findNodeById(dataSource, nodeId);
+                                            if (node) {
+                                                const childrenIds = getAllChildrenIds(node);
+                                                return keys.filter(key => key !== nodeId && !childrenIds.includes(key));
+                                            }
+                                            return keys.filter(key => key !== nodeId);
+                                        };
+                                        set_allParentsKeys(removeNodeAndChildren(reportId, allParentsKeys));
+                                    } else {
+                                        // 展开时,只添加当前节点
+                                        set_allParentsKeys([...allParentsKeys, reportId]);
+                                    }
+                                },
+                            }} 
+                            columns={columns as ProColumns[]} 
+                            scroll={{ y: `calc(100vh - 343px)` }} 
+                            actionRef={tableRef} 
+                            rowKey='reportId'
+                            params={tableDataFilterParams} 
+                            request={(params) => getTableData(params)} 
+                        />
+                    </div>
+                </div>
+            </div>
+        </KCIMPagecontainer>
+    )
+} 

+ 18 - 0
src/pages/hospitalProfitAndLoss/service.ts

@@ -0,0 +1,18 @@
+/*
+ * @Author: code4eat awesomedema@gmail.com
+ * @Date: 2025-01-20 10:00:00
+ * @LastEditors: code4eat awesomedema@gmail.com
+ * @LastEditTime: 2025-07-29 10:12:03
+ * @FilePath: /CostAccountingSys/src/pages/hospitalProfitAndLoss/service.ts
+ * @Description: 全院损益报表查询界面服务
+ */
+
+import { request } from 'umi';
+
+// 获取全院损益数据列表
+export const getBatchHospProfitList = (params: any) => {
+  return request('/costAccount/hospProfitAndLoss/getBatchHospProfitList', {
+    method: 'GET',
+    params: { ...params },
+  });
+}; 

+ 110 - 0
src/pages/hospitalProfitAndLoss/style.less

@@ -0,0 +1,110 @@
+.HospitalProfitAndLoss {
+  background: #FFFFFF;
+  border-radius: 4px;
+
+  .header {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    align-items: center;
+    border-radius: 4px;
+    padding: 16px;
+    background-color: #fff;
+    border-bottom: 16px solid #F7F9FC;
+  }
+
+  .content {
+    padding: 16px;
+    padding-top: 0;
+
+    .inner {
+      display: flex;
+      width: 100%;
+      flex-direction: row;
+      justify-content: flex-start;
+      align-items: flex-start;
+
+      .right {
+        width: 100%;
+
+        .toolBar {
+          display: flex;
+          flex-direction: row;
+          justify-content: space-between;
+          align-items: center;
+          margin-bottom: 12px;
+
+          .filterItem {
+            display: flex;
+            flex-direction: row;
+            justify-content: flex-start;
+            align-items: center;
+
+            .label {
+              margin-right: 8px;
+              font-size: 14px;
+              color: #17181A;
+            }
+          }
+
+          .btnGroup {
+            &>span {
+              cursor: pointer;
+              display: inline-block;
+              color: #17181A;
+              line-height: 24px;
+              padding: 0 14px;
+              border-radius: 4px;
+              border: 1px solid #DAE2F2;
+              background: #FAFCFF;
+              margin-right: 8px;
+              font-weight: 400;
+              font-size: 14px;
+              color: #17181A;
+
+              &.calc {
+                color: #fff;
+                background: #3377FF;
+                border: none;
+              }
+            }
+          }
+        }
+
+        .KCIMTable {
+          .hover-row {
+            .hover-icon {
+              visibility: hidden;
+            }
+
+            &:hover {
+              .hover-icon {
+                visibility: visible;
+              }
+            }
+          }
+
+          .cost-ant-table-header {
+            .cost-ant-table-thead {
+              .cost-ant-table-cell {}
+            }
+          }
+
+          .cost-ant-table-body {
+            .cost-ant-table-row {
+              &.has-children {
+                td {
+                  border-right: none !important;
+                }
+
+                .cost-ant-table-cell-with-append {
+                  font-weight: bold;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+} 

+ 68 - 15
src/pages/monthlyInfoCollection/components/leftAndRighrStructure.tsx

@@ -2,7 +2,7 @@
  * @Author: code4eat awesomedema@gmail.com
  * @Date: 2024-03-19 10:55:39
  * @LastEditors: code4eat awesomedema@gmail.com
- * @LastEditTime: 2024-09-09 10:41:16
+ * @LastEditTime: 2025-08-29 16:49:38
  * @FilePath: /CostAccountingSys/src/pages/monthlyInfoCollection/components/leftAndRighrStructure.tsx
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */
@@ -57,10 +57,15 @@ export const LeftAndRighrStructure = (props: LeftAndRighrStructure) => {
     const [tableSearchKey, set_tableSearchKey] = useState('');
 
     const [leftListData, set_leftListData] = useState<any[]>([]);
+    const [leftListPage, set_leftListPage] = useState<number>(1);
+    const [leftListPageSize, set_leftListPageSize] = useState<number>(200);
+    const [leftListHasMore, set_leftListHasMore] = useState<boolean>(true);
+    const [leftListLoading, set_leftListLoading] = useState<boolean>(false);
     const [ifShowGetBtn, set_ifShowGetBtn] = useState(false);
     const [ifShowCancelBtn, set_ifShowCancelBtn] = useState(false);
 
     const [leftListSearchPlaceHolder, set_leftListSearchPlaceHolder] = useState('请输入');
+    const [leftListSearchKeyword, set_leftListSearchKeyword] = useState('');
 
     const [leftContentH, set_leftContentH] = useState<string|undefined>(undefined);
     const [currentResp, set_currentResp] = useState<any>(undefined);
@@ -157,18 +162,49 @@ export const LeftAndRighrStructure = (props: LeftAndRighrStructure) => {
         }
     }
 
-    const getPatientChargeItemReq =async () => {
-        const resp = await getPatientChargeItemList(computeDate);
-        if(resp){
-            const arr = resp.map((a: any) => ({
+    const getPatientChargeItemReq = async (page:number, pageSize:number, append:boolean=false, filter?:string) => {
+        set_leftListLoading(true);
+        try{
+            const resp:any = await getPatientChargeItemList(computeDate, page, pageSize, filter ?? leftListSearchKeyword);
+            const rawList = Array.isArray(resp) ? resp : (resp?.list || []);
+            const arr = rawList.map((a: any) => ({
                 name: a.name,
                 subText: a.patientNo,
-
             }));
-            set_leftListData([...arr]);
+            if(append){
+                set_leftListData(prev=>[...prev, ...arr]);
+            }else{
+                set_leftListData([...arr]);
+            }
+            const prevCount = append ? leftListData.length : 0;
+            const totalCount = (resp && typeof resp === 'object') ? (resp.totalCount ?? resp.total ?? undefined) : undefined;
+            if(typeof totalCount === 'number'){
+                set_leftListHasMore(prevCount + arr.length < totalCount);
+            }else{
+                set_leftListHasMore(arr.length === pageSize);
+            }
+            set_leftListPage(page);
+        } finally{
+            set_leftListLoading(false);
         }
     }
 
+    const onReachBottomLoadMore = () => {
+        if(tabKey !== '6') return;
+        if(leftListLoading) return;
+        if(!leftListHasMore) return;
+        const nextPage = leftListPage + 1;
+        getPatientChargeItemReq(nextPage, leftListPageSize, true);
+    }
+
+    const onLeftSearchChange = (keyword:string) => {
+        if(tabKey !== '6') return;
+        set_leftListSearchKeyword(keyword);
+        set_leftListPage(1);
+        set_leftListHasMore(true);
+        getPatientChargeItemReq(1, leftListPageSize, false, keyword);
+    }
+
     const getLeftDatas = ()=>{
         if (tabKey == '1') {
             inComeLeftData();
@@ -183,7 +219,7 @@ export const LeftAndRighrStructure = (props: LeftAndRighrStructure) => {
             getPatientInfoDepartments();
         }
         if(tabKey == '6'){
-            getPatientChargeItemReq()
+            getPatientChargeItemReq(1, leftListPageSize, false, leftListSearchKeyword)
         }
     }
 
@@ -199,11 +235,9 @@ export const LeftAndRighrStructure = (props: LeftAndRighrStructure) => {
             set_scrollY(`calc(100vh - 395px)`);
             set_tabSearchKey('name');
             set_tableSearchKey('filter');
-            inComeLeftData();
             set_leftContentH(`calc(100vh - 380px)`);
         }
         if (tabKey == '3') {
-            costShareLeftData();
             set_tabSearchKey('responsibilityName');
             set_scrollX(undefined);
             set_leftContentH('calc(100vh - 294px)');
@@ -215,7 +249,6 @@ export const LeftAndRighrStructure = (props: LeftAndRighrStructure) => {
             set_scrollY(`calc(100vh - 396px)`);
             set_tabSearchKey('name');
             set_tableSearchKey('filter');
-            costLeftData();
             set_leftContentH('calc(100vh - 351px)');
         }
 
@@ -224,7 +257,6 @@ export const LeftAndRighrStructure = (props: LeftAndRighrStructure) => {
             set_scrollY(`calc(100vh - 345px)`);
             set_tabSearchKey('name');
             set_tableSearchKey('');
-            getPatientInfoDepartments();
             set_leftContentH(`calc(100vh - 324px)`);
         }
         if (tabKey == '6') {
@@ -232,8 +264,8 @@ export const LeftAndRighrStructure = (props: LeftAndRighrStructure) => {
             set_tabSearchKey('name');
             set_tableSearchKey('filter');
             set_leftListSearchPlaceHolder('患者ID/姓名');
-            getPatientChargeItemReq();
             set_leftContentH(`calc(100vh - 292px)`);
+            set_leftListSearchKeyword('');
         }
 
     }, [tabKey]);
@@ -258,9 +290,26 @@ export const LeftAndRighrStructure = (props: LeftAndRighrStructure) => {
 
 
     useEffect(() => {
-        getLeftDatas();
+        // 合并为一次驱动:当 computeDate 或 tabKey 变化时,按分支加载一次左侧数据
+        if (tabKey == '1') {
+            inComeLeftData();
+        }
+        if (tabKey == '2') {
+            costLeftData();
+        }
+        if (tabKey == '3') {
+            costShareLeftData();
+        }
+        if (tabKey == '5') {
+            getPatientInfoDepartments();
+        }
+        if (tabKey == '6') {
+            set_leftListPage(1);
+            set_leftListHasMore(true);
+            getPatientChargeItemReq(1, leftListPageSize, false, leftListSearchKeyword);
+        }
         tableRef.current?.reload();
-    }, [computeDate]);
+    }, [computeDate, tabKey]);
 
     useEffect(() => {
         if (btnPerm) {
@@ -356,6 +405,10 @@ export const LeftAndRighrStructure = (props: LeftAndRighrStructure) => {
                         dataSource={leftListData} searchKey={tabSearchKey} onChange={onLeftChange}
                         placeholder={leftListSearchPlaceHolder} listType={tabKey == '3' ? 'tree' : 'list'}
                         icon={tabKey == '6' ? require('../../../../static/empInfo.png') : undefined}
+                        onReachBottom={tabKey=='6'? onReachBottomLoadMore: undefined}
+                        loading={leftListLoading}
+                        useRemoteSearch={tabKey=='6'}
+                        onSearchChange={onLeftSearchChange}
                     />
                 </div>
                 <div className="right">

+ 3 - 3
src/pages/monthlyInfoCollection/index.tsx

@@ -2,7 +2,7 @@
  * @Author: code4eat awesomedema@gmail.com
  * @Date: 2024-03-18 15:52:26
  * @LastEditors: code4eat awesomedema@gmail.com
- * @LastEditTime: 2024-09-25 14:56:09
+ * @LastEditTime: 2025-08-29 15:53:57
  * @FilePath: /CostAccountingSys/src/pages/monthlyInfoCollection/index.tsx
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */
@@ -193,8 +193,8 @@ const MonthlyInfoCollection = () => {
                         items={tabs}
                         key={'key'}
                         onChange={(key) => onTabChanged(key)}
-                    />
-                </div>
+                    />  
+                </div>  
 
                 <div className="tabContent">
                     {currentTabKey == '0' && <IncomeCostDataImport date={computeDate} btnPerm={currentSelectedTab ? currentSelectedTab.expandTwo : '0|0'} />}

+ 3 - 3
src/pages/monthlyInfoCollection/service.ts

@@ -60,11 +60,11 @@ export const getPatientInfoDepartmentList = (computeDate:string,type:number) =>
     });
 };
 
-//获取患者收费项目列表
-export const getPatientChargeItemList = (computeDate:string) => {
+//获取患者收费项目列表(分页,支持filter)
+export const getPatientChargeItemList = (computeDate:string, current:number, pageSize:number, filter?:string) => {
     return request('/costAccount/costData/getPatientItemPatientInfo', {
       method: 'GET',
-      params:{computeDate}
+      params:{computeDate, current, pageSize, filter}
     });
 };
 

+ 267 - 47
src/pages/reportExport/report/index.tsx

@@ -2,7 +2,7 @@
  * @Author: code4eat awesomedema@gmail.com
  * @Date: 2023-01-04 14:12:31
  * @LastEditors: code4eat awesomedema@gmail.com
- * @LastEditTime: 2025-02-27 17:09:22
+ * @LastEditTime: 2025-08-20 11:11:32
  * @FilePath: /BudgetManaSystem/src/pages/budgetMana/oneBatch/index.tsx
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */
@@ -111,11 +111,18 @@ const ReportExport = ({ reportCode, propsParams, tableScrollH }: { reportCode: s
   const [tableData, set_tableData] = useState<any[]>([]);
   const [searchType, set_searchType] = useState(0);
   const [pagination, set_pagination] = useState<any>(null);
+  const [frontendCurrentPage, set_frontendCurrentPage] = useState<number>(1); // 前端分页当前页码
+  const currentPageRef = useRef<number>(1); // 使用ref保存实时的当前页码,避免状态更新延迟
 
 
   const reportJumphandle = (reportData: any) => {
     set_loading(true);
     set_searchRows([]);
+    
+    // 重要:在重置之前先保存当前的分页信息
+    const currentPageNum = currentPageRef.current;
+    const currentPageSize = pagination?.pageSize || pageSize;
+    
     let parameter: { [key: string]: any } = {};
     const { redirectParameter = undefined } = reportData;
     const _step = step + 1;
@@ -127,8 +134,26 @@ const ReportExport = ({ reportCode, propsParams, tableScrollH }: { reportCode: s
       });
     }
 
+    // 获取当前分页信息 - 使用ref获取实时页码,避免状态延迟
+
+    // 首先更新当前面包屑项的分页信息 - 确保保存当前页面的分页状态
+    const updatedBreadCrumbList = breadCrumbList.map((item, index) => {
+      if (index === step) {
+        return {
+          ...item,
+          params: {
+            ...item.params,
+            current: currentPageNum || 1, // 确保有默认值
+            pageSize: currentPageSize,
+          }
+        };
+      }
+      return item;
+    });
+
+    // 然后添加新的面包屑项
     set_breadCrumbList(
-      [...breadCrumbList,
+      [...updatedBreadCrumbList,
       {
         index: _step,
         name: reportData[`report_name`] || reportData[`redirectReportName`],
@@ -136,21 +161,31 @@ const ReportExport = ({ reportCode, propsParams, tableScrollH }: { reportCode: s
           reportCode: reportData.redirectReportCode,
           parameter: {
             ...parameter
-          }
+          },
+          // 新报表从第1页开始
+          current: 1,
+          pageSize: currentPageSize,
         }
       }
       ]
     );
 
     set_step(_step);
-    getSearchRows(reportData.redirectReportCode, parameter);
+    
+    // 重置分页状态到第1页(新报表从第1页开始)
+    currentPageRef.current = 1;
+    set_frontendCurrentPage(1);
+    
+    // 新报表从第1页开始
+    getSearchRows(reportData.redirectReportCode, parameter, 1, currentPageSize);
 
     totalTableData = [];
     currentPage = 0;
     dataSource = [];
   }
 
-  const getSearchRows = async (code: string, others?: any) => {
+  // 修改getSearchRows,支持接收current和pageSize参数
+  const getSearchRows = async (code: string, others?: any, current?: number, pageSizeArg?: number) => {
     const resp = await getSearchRowsReq(code);
 
     if (resp) {
@@ -161,7 +196,18 @@ const ReportExport = ({ reportCode, propsParams, tableScrollH }: { reportCode: s
         acc[curr.columnIndex] = '';
         return acc;
       }, {});
-      set_tableDataFilterParams({ ...tableDataFilterParams, reportCode: code, parameter: { ...others, compute_date: currentComputeDate, ...searchObject, ...(reportCode ? tableDataFilterParams?.parameter : {}) } });
+      // 设置tableDataFilterParams时带上current和pageSize,确保正确传递分页参数
+      const finalCurrent = current !== undefined ? current : 1;
+      const finalPageSize = pageSizeArg !== undefined ? pageSizeArg : pageSize;
+      
+
+      
+      set_tableDataFilterParams({
+        reportCode: code,
+        parameter: { ...others, compute_date: currentComputeDate, ...searchObject, ...(reportCode ? tableDataFilterParams?.parameter : {}) },
+        current: finalCurrent,
+        pageSize: finalPageSize,
+      });
     }
   }
 
@@ -173,37 +219,61 @@ const ReportExport = ({ reportCode, propsParams, tableScrollH }: { reportCode: s
     }
 
 
-    if (!params.reportCode || (JSON.stringify(prevParams) === JSON.stringify(params))) {
-      // 参数相同,不执行请求
+    if (!params.reportCode) {
+      // 重要:如果没有reportCode,也要关闭loading状态
+      if (!openProcessModal) {
+        set_loading(false);
+      }
       return;
     }
-
-    const { systemId } = JSON.parse(localStorage.getItem('currentSelectedTab') as string)
-    const paramsData = await getParamsDataBySysId(systemId, '1806523783696224256');
-    const pageSize = paramsData.value ? Number(paramsData.value) : 100;
-    set_pageSize(pageSize);
-    const { parameter = {}, current, reportCode } = params;
-    searchKeys = [];
-    let resp: any = undefined;
-
-
-    if (step != 0) {
-      //报表跳转
-      resp = await getRedirReportData(
-        { pageSize, current },
-        reportCode,
-        { ...parameter, compute_date: currentComputeDate, }
-      );
+    
+    // 检查参数是否完全相同,包括分页参数的比较(导出过程不做去重跳过)
+    if (!openProcessModal &&
+        prevParams && 
+        prevParams.reportCode === params.reportCode && 
+        JSON.stringify(prevParams.parameter) === JSON.stringify(params.parameter) &&
+        prevParams.current === params.current &&
+        prevParams.pageSize === params.pageSize) {
+      // 参数完全相同且非导出场景,不执行请求
+      if (!openProcessModal) {
+        set_loading(false);
+      }
+      return;
     }
 
-    if (step == 0) {
-      //首次获取表格数据
-      resp = await getData(
-        { pageSize, current },
-        urlReportCode as string,
-        { ...parameter, compute_date: currentComputeDate, }
-      );
-    }
+    try {
+      const { systemId } = JSON.parse(localStorage.getItem('currentSelectedTab') as string)
+      const paramsData = await getParamsDataBySysId(systemId, '1806523783696224256');
+      const defaultPageSize = paramsData.value ? Number(paramsData.value) : 100;
+      
+      // 使用传入的参数,如果没有传入则使用默认值
+      const { parameter = {}, current, reportCode, pageSize: paramPageSize } = params;
+      const finalPageSize = paramPageSize || defaultPageSize;
+      
+      set_pageSize(finalPageSize);
+      
+
+      searchKeys = [];  
+      let resp: any = undefined;
+
+
+      if (step != 0) {
+        //报表跳转
+        resp = await getRedirReportData(
+          { pageSize: finalPageSize, current },
+          reportCode,
+          { ...parameter, compute_date: currentComputeDate, }
+        );
+      }
+
+      if (step == 0) {
+        //首次获取表格数据
+        resp = await getData(
+          { pageSize: finalPageSize, current },
+          urlReportCode as string,
+          { ...parameter, compute_date: currentComputeDate, }
+        );
+      }
 
     if (resp) {
       prevParams = params;
@@ -331,12 +401,20 @@ const ReportExport = ({ reportCode, propsParams, tableScrollH }: { reportCode: s
       if (!openProcessModal) {
         set_tableData(data);
         dataSource = data;
+        
+        // 使用传入的current参数,而不是后端响应的current
+        // 这样可以确保面包屑跳转时保持正确的页码
+        const finalCurrent = params.current || current;
+        currentPageRef.current = finalCurrent; // 同步更新ref
+        
         set_pagination({
-          current,
+          current: finalCurrent,
           total: totalCount,
-          pageSize: pageSize,
+          pageSize: finalPageSize, // 使用我们计算的finalPageSize而不是响应中的pageSize
           totalPage: totalPage,
         })
+        
+
 
       }
 
@@ -347,11 +425,20 @@ const ReportExport = ({ reportCode, propsParams, tableScrollH }: { reportCode: s
         dataSource = [];
         set_pagination(false)
       }
-
     }
+      } catch (error) {
+      prevParams = undefined;
+    if (!openProcessModal) {
+      set_tableData([]);
+      dataSource = [];
+      set_pagination(false);
+    }
+  } finally {
+    // 确保无论如何都要关闭loading状态
     if (!openProcessModal) {
       set_loading(false);
     }
+  }
     return openProcessModal ? { currentCount: 0, totalCount: 0 } : []
   }
 
@@ -424,19 +511,66 @@ const ReportExport = ({ reportCode, propsParams, tableScrollH }: { reportCode: s
   };
 
   const handleCompletion = () => {
+    // 关闭弹窗并执行导出
     set_openProcessModal(false);
     exportHandle();
-
+    // 导出完成后,重置导出相关游标,避免下次导出受影响
+    currentPage = 0;
+    maxFetchCount = 0;
   }
 
 
   //面包屑跳转
   const switchHandle = (data: any) => {
     set_loading(true);
-    set_step(data.index);
-    const _breadCrumbList = breadCrumbList.filter((a: any) => a.index <= data.index);
-    set_breadCrumbList([..._breadCrumbList]);
-    getSearchRows(data.params.reportCode);
+    
+    try {
+      // 在跳转前,先保存当前页面的分页状态
+      const currentPageNum = pagination?.current || 1;
+      const currentPageSize = pagination?.pageSize || pageSize;
+      
+      // 更新当前面包屑项的分页信息
+      const updatedBreadCrumbList = breadCrumbList.map((item, index) => {
+        if (index === step) {
+          return {
+            ...item,
+            params: {
+              ...item.params,
+              current: currentPageNum,
+              pageSize: currentPageSize,
+            }
+          };
+        }
+        return item;
+      });
+      
+      set_step(data.index);
+      const _breadCrumbList = updatedBreadCrumbList.filter((a: any) => a.index <= data.index);
+      set_breadCrumbList([..._breadCrumbList]);
+      
+      // 恢复分页信息,并传递给getSearchRows
+      const { reportCode, parameter, current, pageSize: savedPageSize } = data.params;
+      
+
+      
+      // 恢复分页状态 - 确保使用保存的分页信息
+      const restoredCurrent = current || 1;
+      const restoredPageSize = savedPageSize || pageSize;
+      
+      currentPageRef.current = restoredCurrent; // 更新ref
+      set_frontendCurrentPage(restoredCurrent);
+      
+      // 确保正确传递分页参数,避免页码重置
+      getSearchRows(reportCode, parameter, restoredCurrent, restoredPageSize);
+      
+      // 同时重置全局变量以避免数据污染
+      totalTableData = [];
+      currentPage = 0;
+      dataSource = [];
+    } catch (error) {
+      // 发生错误时也要关闭loading状态
+      set_loading(false);
+    }
   }
 
 
@@ -506,11 +640,37 @@ const ReportExport = ({ reportCode, propsParams, tableScrollH }: { reportCode: s
     // 在这里处理路由参数变化的逻辑
     set_loading(true);
     set_tableDataFilterParams(undefined);
-    set_breadCrumbList([{
-      name: '首页',
-      index: 0,
-      params: { reportCode: urlReportCode, parameter: { ...(reportCode ? tableDataFilterParams?.parameter : {}), compute_date: currentComputeDate } }
-    }]);
+    
+    // 检查是否已有面包屑信息,如果有则保留,否则初始化
+    if (breadCrumbList.length === 0) {
+      // 初始化时带上分页信息
+      set_breadCrumbList([{
+        name: '首页',
+        index: 0,
+        params: { 
+          reportCode: urlReportCode, 
+          parameter: { ...(reportCode ? tableDataFilterParams?.parameter : {}), compute_date: currentComputeDate }, 
+          current: 1, 
+          pageSize 
+        }
+      }]);
+    } else {
+      // 如果已有面包屑,只更新首页的reportCode和parameter,保留分页信息
+      const updatedBreadCrumbList = breadCrumbList.map((item, index) => {
+        if (index === 0) {
+          return {
+            ...item,
+            params: {
+              ...item.params,
+              reportCode: urlReportCode,
+              parameter: { ...(reportCode ? tableDataFilterParams?.parameter : {}), compute_date: currentComputeDate }
+            }
+          };
+        }
+        return item;
+      });
+      set_breadCrumbList(updatedBreadCrumbList);
+    }
 
     urlReportCode && getSearchRows(urlReportCode);
 
@@ -549,6 +709,15 @@ const ReportExport = ({ reportCode, propsParams, tableScrollH }: { reportCode: s
     }
   }, [])
 
+  useEffect(() => {
+    if (openProcessModal) {
+      // 打开导出弹窗时重置累计数据与翻页游标
+      totalTableData = [];
+      currentPage = 0;
+      maxFetchCount = 0;
+    }
+  }, [openProcessModal])
+
 
   return (
     <KCIMPagecontainer className='ReportTemplate' title={false} style={reportCode ? { border: 'none' } : {}}>
@@ -670,9 +839,60 @@ const ReportExport = ({ reportCode, propsParams, tableScrollH }: { reportCode: s
             loading={loading}
             scroll={{ x: tableScrollX, y: tableH }}
             dataSource={tableData}
-            pagination={searchType == 1 ? { pageSizeOptions: [10, 20, 50, 100, 1000], showSizeChanger: true, pageSize } : {
+            pagination={searchType == 1 ? { 
+              pageSizeOptions: [10, 20, 50, 100, 1000], 
+              showSizeChanger: true, 
+              pageSize,
+              onChange: (page: number, pageSize: number) => {
+                currentPageRef.current = page; // 立即更新ref
+                set_frontendCurrentPage(page);
+                
+                // 同步更新面包屑信息
+                const updatedBreadCrumbList = breadCrumbList.map((item, index) => {
+                  if (index === step) {
+                    return {
+                      ...item,
+                      params: {
+                        ...item.params,
+                        current: page,
+                        pageSize: pageSize
+                      }
+                    };
+                  }
+                  return item;
+                });
+                set_breadCrumbList(updatedBreadCrumbList);
+                
+                // 发起数据请求
+                getTableData({ ...tableDataFilterParams, current: page, pageSize });
+              }
+            } : {
               ...pagination,
               onChange(page, pageSize) {
+                // 立即更新ref和pagination状态
+                currentPageRef.current = page; // 立即更新ref
+                set_pagination((prev: any) => ({
+                  ...prev,
+                  current: page,
+                  pageSize: pageSize
+                }));
+                
+                // 更新当前面包屑项的分页信息
+                const updatedBreadCrumbList = breadCrumbList.map((item, index) => {
+                  if (index === step) {
+                    return {
+                      ...item,
+                      params: {
+                        ...item.params,
+                        current: page,
+                        pageSize: pageSize
+                      }
+                    };
+                  }
+                  return item;
+                });
+                set_breadCrumbList(updatedBreadCrumbList);
+                
                 getTableData({ ...tableDataFilterParams, current: page, pageSize });
               },
             }}

+ 114 - 26
src/pages/specialDataImport/index.tsx

@@ -2,7 +2,7 @@
  * @Author: code4eat awesomedema@gmail.com
  * @Date: 2023-03-03 11:30:33
  * @LastEditors: code4eat awesomedema@gmail.com
- * @LastEditTime: 2024-09-25 15:02:26
+ * @LastEditTime: 2025-07-11 14:47:55
  * @FilePath: /KC-MiddlePlatform/src/pages/platform/setting/pubDicTypeMana/index.tsx
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */
@@ -13,12 +13,12 @@
 import { createFromIconfontCN } from '@ant-design/icons';
 
 import { ActionType } from '@ant-design/pro-components';
-import { ModalForm, ProFormDatePicker, ProFormSelect, ProFormText, ProFormTextArea } from '@ant-design/pro-form'
+import { ModalForm, ProFormDatePicker, ProFormSelect, ProFormText, ProFormTextArea, ProFormRadio, ProFormDependency } from '@ant-design/pro-form'
 import { ProColumns } from '@ant-design/pro-table';
-import { message, Popconfirm, DatePicker } from 'antd';
+import { message, Popconfirm, DatePicker, Modal } from 'antd';
 import { useEffect, useRef, useState } from 'react'
 
-import { afterImport, getData, getSpecialImportTable, tableDataImport } from './service';
+import { afterImport, getData, getSpecialImportTable, tableDataImport, importMultipleSheets } from './service';
 import FormItem from 'antd/es/form/FormItem';
 
 import './style.less';
@@ -111,6 +111,43 @@ export default function DicClassfication() {
         }
     }
 
+    // 显示导入错误详情的Modal
+    const showImportErrorModal = (failedImports: any[], successCount: number) => {
+        const errorContent = failedImports.map((item, index) => (
+            <div key={index} style={{ marginBottom: 12, padding: 12, backgroundColor: '#fff2f0', border: '1px solid #ffccc7', borderRadius: 6 }}>
+                <div style={{ fontWeight: 'bold', color: '#cf1322', marginBottom: 4 }}>
+                    Sheet: {item.sheetName}
+                </div>
+                <div style={{ color: '#595959', fontSize: 13 }}>
+                    错误信息: {item.errorMessage}
+                </div>
+            </div>
+        ));
+
+        Modal.error({
+            title: '导入结果详情',
+            width: 600,
+            content: (
+                <div>
+                    {successCount > 0 && (
+                        <div style={{ marginBottom: 16, padding: 12, backgroundColor: '#f6ffed', border: '1px solid #b7eb8f', borderRadius: 6 }}>
+                            <span style={{ color: '#52c41a', fontWeight: 'bold' }}>
+                                成功导入: {successCount} 个Sheet
+                            </span>
+                        </div>
+                    )}
+                    <div style={{ marginBottom: 8, fontWeight: 'bold', color: '#cf1322' }}>
+                        失败导入: {failedImports.length} 个Sheet
+                    </div>
+                    <div style={{ maxHeight: 400, overflowY: 'auto' }}>
+                        {errorContent}
+                    </div>
+                </div>
+            ),
+            okText: '知道了',
+        });
+    }
+
 
     const getTableData = async (params: any) => {
         const { computeDate } = params;
@@ -137,7 +174,7 @@ export default function DicClassfication() {
         return (
             <ModalForm
                 className='SpecialDataImport-Modal'
-                width={360}
+                width={368}
                 title={`导入数据`}
                 trigger={
                     <span key="3" className='importBtn'>导入</span>
@@ -154,37 +191,88 @@ export default function DicClassfication() {
                 }}
                 onFinish={async (values) => {
                     // console.log({values});
-                    const { importFile: { fileList } } = values;
+                    const { importFile: { fileList }, importMode } = values;
 
                     let formData = new FormData();
                     formData.append('file', fileList[0].originFileObj);
                     formData.append('computeDate', values.computeDate);
-                    formData.append('tableName', values.tableName);
-
-                    const resp = await tableDataImport(formData);
-
-                    if (resp) {
-                        tableRef.current?.reload();
-                        return true;
+                    
+                    let resp;
+                    if (importMode === 'single') {
+                        // 单页导入
+                        formData.append('tableName', values.tableName);
+                        resp = await tableDataImport(formData);
+                        if (resp) {
+                            message.success('导入成功!');
+                            tableRef.current?.reload();
+                            return true;
+                        } else {
+                            message.error('导入失败,请检查文件格式和内容');
+                        }
+                    } else {
+                        // 多页导入
+                        resp = await importMultipleSheets(formData);
+                        if (resp && Array.isArray(resp)) {
+                            // 检查是否有失败的导入
+                            const failedImports = resp.filter(item => item.success === false);
+                            const successImports = resp.filter(item => item.success === true);
+                            
+                            if (failedImports.length > 0) {
+                                // 有失败的导入,显示详细错误信息
+                                showImportErrorModal(failedImports, successImports.length);
+                            } else {
+                                // 全部成功
+                                message.success('所有Sheet导入成功!');
+                            }
+                            
+                            tableRef.current?.reload();
+                            return true;
+                        }
                     }
 
+                    return false;
                 }}
             >
                 <div className='tip'>本操作为追加导入,会保留已有的数据并将文件的所有记录追加到数据表</div>
-                <ProFormDatePicker.Month label='所属年月' name='computeDate' width={326} rules={[{ required: true, message: '所属年月不能为空!' }]} fieldProps={{autoComplete:'off'}} />
-                <ProFormSelect label='数据库表名称' name='tableName' width={326}
-                    request={async () => {
-                        const resp = await getSpecialImportTable();
-                        if (resp) {
-
-                            return resp.map((a: any) => ({
-                                label: a.name,
-                                value: a.code
-                            }))
+                <ProFormDatePicker.Month label='所属年月' name='computeDate' width={326} rules={[{ required: true, message: '所属年月不能为空!' }]} fieldProps={{ autoComplete: 'off' }} />
+                
+                <ProFormRadio.Group
+                    name="importMode"
+                    label="导入方式"
+                    tooltip="单页导入:一个Excel文件中对应一个目标数据表,需要手动选择数据库表名称;多页导入:一个Excel文件的个Sheet页对应一个目标数据表,Sheet页名称需要和数据库表名完全一致"
+                    initialValue="single"
+                    options={[
+                        { label: '单页导入', value: 'single' },
+                        { label: '多页导入', value: 'multiple' },
+                    ]}
+                />
+                
+                <ProFormDependency name={['importMode']}>
+                    {({ importMode }) => {
+                        if (importMode === 'single') {
+                            return (
+                                <ProFormSelect 
+                                    label='数据库表名称' 
+                                    name='tableName' 
+                                    width={326}
+                                    request={async () => {
+                                        const resp = await getSpecialImportTable();
+                                        if (resp) {
+                                            return resp.map((a: any) => ({
+                                                label: a.name,
+                                                value: a.code
+                                            }))
+                                        }
+                                        return []
+                                    }}
+                                    rules={[{ required: true, message: '数据库表名称不能为空!' }]} 
+                                />
+                            );
                         }
-                        return []
+                        return null;
                     }}
-                    rules={[{ required: true, message: '数据库表名称不能为空!' }]} />
+                </ProFormDependency>
+                
                 <FormItem name={'importFile'}>
                     {currentComputeDate && <KCIMUpload downloadTemplateFile={() => downloadTemplate()} ifShowTip={false} ifShowTemplateDownload={false} />}
                 </FormItem>
@@ -204,7 +292,7 @@ export default function DicClassfication() {
                             <DatePicker
                                 onChange={(data, dateString) => {
                                     set_tableDataFilterParams({ ...tableDataFilterParams, computeDate: dateString });
-                                    setInitialState((s:any)=>({...s,currentComputeDate:dateString}));
+                                    setInitialState((s: any) => ({ ...s, currentComputeDate: dateString }));
                                 }}
                                 picker='month'
                                 locale={locale}

+ 9 - 0
src/pages/specialDataImport/service.ts

@@ -42,6 +42,15 @@ export const tableDataImport = (data:any) => {
   });
 };
 
+//多页导入
+
+export const importMultipleSheets = (data:any) => {
+  return request('/costAccount/import/importMultipleSheets', {
+    method: 'POST',
+    data
+  });
+};
+
 //导入后的撤销/复原操作
 
 export const afterImport = (id:number,type:'CANCEL'|'RECOVERY') => {

+ 1 - 1
src/services/getDic.ts

@@ -2,7 +2,7 @@
  * @Author: code4eat awesomedema@gmail.com
  * @Date: 2023-04-20 14:06:17
  * @LastEditors: code4eat awesomedema@gmail.com
- * @LastEditTime: 2024-06-28 14:47:20
+ * @LastEditTime: 2025-07-10 14:23:52
  * @FilePath: /BudgetManaSystem/src/services/getDic.ts
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */

+ 39 - 9
src/utils/exporter.ts

@@ -5,6 +5,7 @@
 
 import { ProColumns } from '@ant-design/pro-components';
 import exportTableToExcel from './tableToExcel';
+import exportTableToMultiExcel from './tableToMultiHeaderExcel';
 import { formatMoneyNumber, formatToPercentage } from './format';
 
 export interface ExportOptions {
@@ -25,15 +26,24 @@ export function exportCalcPageDataToExcel(
 ) {
   const { calcPageKey, indentColumn } = options;
 
-  // 生成表头
-  const headers: { [key: string]: any } = {};
-  columns.forEach((a: any) => {
-    headers[`${a.dataIndex}`] = a.title;
-  });
+  // 扁平化列(仅保留叶子列,保证多级表头时也能拿到 dataIndex)
+  const flattenColumns = (cols: any[]): any[] => {
+    const result: any[] = [];
+    (cols || []).forEach((c: any) => {
+      if (Array.isArray(c?.children) && c.children.length > 0) {
+        result.push(...flattenColumns(c.children));
+      } else if (c?.dataIndex) {
+        result.push(c);
+      }
+    });
+    return result;
+  };
+  const leafColumns: any[] = flattenColumns(columns as any[]);
+  const flatKeys: string[] = leafColumns.map((c: any) => String(c.dataIndex));
 
-  // 列配置索引,便于判断数值列
+  // 列配置索引,便于判断数值列(基于叶子列)
   const columnMap = new Map<string, any>();
-  (columns as any[])?.forEach?.((c) => {
+  (leafColumns as any[])?.forEach?.((c) => {
     if (c && c.dataIndex) columnMap.set(String(c.dataIndex), c);
   });
 
@@ -42,7 +52,7 @@ export function exportCalcPageDataToExcel(
   const processData = (items: any[], level: number = 0) => {
     items.forEach(item => {
       const row: { [key: string]: any } = {};
-      Object.keys(headers).forEach(key => {
+      flatKeys.forEach(key => {
         if (indentColumn && key === indentColumn) {
           // 使用 Unicode 不可见空格来缩进
           row[`${key}`] = `${'\u00A0'.repeat(level * 4)}${item[`${key}`]}`;
@@ -111,9 +121,29 @@ export function exportCalcPageDataToExcel(
     deptCostAllocationSummary: '医院科室成本分摊汇总表',
     hospitalVisitCostComposition: '医院诊次成本构成表',
     hospitalBedDayCostComposition: '医院床日成本构成表',
+    hospitalDeptVisitCost: '医院科室诊次成本表',
+    hospitalDeptBedDayCost: '医院科室床日成本表',
+    hospitalServiceProjectCost: '医院医疗服务项目成本汇总表',
+    medicalServiceCostDetail: '医院医疗服务项目成本明细表',
+    diseaseCostDetail: '医院病种成本明细表',
+    diseaseCostCompositionDetail: '病种成本构成明细表',
+    drgCostDetail: '医院DRG成本明细表',
+    drgCostCompositionDetail: '医院DRG成本构成明细表',
+    deptDiseaseCostCompositionDetail: '服务单元病种成本构成明细表',
+    deptDrgCostCompositionDetail: '医院服务单元DRG成本构成明细表',
   };
   const fileName = (calcPageKey && fileNameMap[calcPageKey]) ? fileNameMap[calcPageKey] : '项目成本计算';
-  exportTableToExcel(data, columns as any[], fileName);
+  
+  // 判断是否为多级表头报表
+  const hasMultiLevelHeader = (columns as any[])?.some?.((col: any) => Array.isArray(col?.children) && col.children.length > 0);
+  
+  if (hasMultiLevelHeader) {
+    // 使用多级表头导出
+    exportTableToMultiExcel(data, columns as any[], fileName);
+  } else {
+    // 使用简单表头导出
+    exportTableToExcel(data, columns as any[], fileName);
+  }
 }
 
 

+ 4 - 1
typings.d.ts

@@ -15,4 +15,7 @@ export type TableDataResponse = {
     total: number; 
     pageSize: number; 
     totalPage: number;
-};
+};
+
+// xlsx-js-style 无官方 TS 声明,增加模块声明以消除类型报错
+declare module 'xlsx-js-style';

+ 63 - 1
yarn.lock

@@ -3060,6 +3060,14 @@ add-dom-event-listener@^1.1.0:
   dependencies:
     object-assign "4.x"
 
+adler-32@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.2.0.tgz#6a3e6bf0a63900ba15652808cb15c6813d1a5f25"
+  integrity sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ==
+  dependencies:
+    exit-on-epipe "~1.0.1"
+    printj "~1.1.0"
+
 adler-32@~1.3.0:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.3.1.tgz#1dbf0b36dda0012189a32b3679061932df1821e2"
@@ -3693,7 +3701,7 @@ caniuse-lite@^1.0.30001503:
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001512.tgz#7450843fb581c39f290305a83523c7a9ef0d4cb4"
   integrity sha512-2S9nK0G/mE+jasCUsMPlARhRCts1ebcp2Ji8Y8PWi4NDE1iRdLCnEPHkEfeBrGC45L4isBx5ur3IQ6yTE2mRZw==
 
-cfb@~1.2.1:
+cfb@^1.1.4, cfb@~1.2.1:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.2.tgz#94e687628c700e5155436dac05f74e08df23bc44"
   integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==
@@ -3811,6 +3819,14 @@ clone-regexp@^2.1.0:
   dependencies:
     is-regexp "^2.0.0"
 
+codepage@~1.14.0:
+  version "1.14.0"
+  resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.14.0.tgz#8cbe25481323559d7d307571b0fff91e7a1d2f99"
+  integrity sha512-iz3zJLhlrg37/gYRWgEPkaFTtzmnEv1h+r7NgZum2lFElYQPi0/5bnmuDfODHxfp0INEfnRqyfyeIJDbb7ahRw==
+  dependencies:
+    commander "~2.14.1"
+    exit-on-epipe "~1.0.1"
+
 codepage@~1.15.0:
   version "1.15.0"
   resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.15.0.tgz#2e00519024b39424ec66eeb3ec07227e692618ab"
@@ -3882,6 +3898,16 @@ commander@^9.4.1:
   resolved "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz"
   integrity sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==
 
+commander@~2.14.1:
+  version "2.14.1"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa"
+  integrity sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==
+
+commander@~2.17.1:
+  version "2.17.1"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
+  integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
+
 common-path-prefix@^3.0.0:
   version "3.0.0"
   resolved "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz"
@@ -4913,6 +4939,11 @@ execall@^2.0.0:
   dependencies:
     clone-regexp "^2.1.0"
 
+exit-on-epipe@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692"
+  integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==
+
 ext@^1.1.2:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f"
@@ -4978,6 +5009,11 @@ fetch-blob@^3.1.2, fetch-blob@^3.1.4:
     node-domexception "^1.0.0"
     web-streams-polyfill "^3.0.3"
 
+fflate@^0.3.8:
+  version "0.3.11"
+  resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.3.11.tgz#2c440d7180fdeb819e64898d8858af327b042a5d"
+  integrity sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A==
+
 file-entry-cache@^6.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@@ -7478,6 +7514,11 @@ pretty-error@^4.0.0:
     lodash "^4.17.20"
     renderkid "^3.0.0"
 
+printj@~1.1.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
+  integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
+
 process-nextick-args@~2.0.0:
   version "2.0.1"
   resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz"
@@ -8155,6 +8196,11 @@ react-sortable-hoc@^2.0.0:
     invariant "^2.2.4"
     prop-types "^15.5.7"
 
+react-window@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/react-window/-/react-window-2.1.2.tgz#f18900ab9e60a7c6549e670cded4845f656ae382"
+  integrity sha512-3PnhB1bXauRVTR1vVwEmFjbaNDCIubOoNLTvvHrOI9cGOkPGb5XAzlprNN/FuUlnKsaaws31t3IJYbJJvhJcBQ==
+
 react@18.1.0:
   version "18.1.0"
   resolved "https://registry.npmjs.org/react/-/react-18.1.0.tgz"
@@ -9630,6 +9676,22 @@ write-file-atomic@^4.0.1, write-file-atomic@^4.0.2:
     imurmurhash "^0.1.4"
     signal-exit "^3.0.7"
 
+xlsx-js-style@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/xlsx-js-style/-/xlsx-js-style-1.2.0.tgz#58455f2fd3c5e22807c2841f5b0631a07098b719"
+  integrity sha512-DDT4FXFSWfT4DXMSok/m3TvmP1gvO3dn0Eu/c+eXHW5Kzmp7IczNkxg/iEPnImbG9X0Vb8QhROda5eatSR/97Q==
+  dependencies:
+    adler-32 "~1.2.0"
+    cfb "^1.1.4"
+    codepage "~1.14.0"
+    commander "~2.17.1"
+    crc-32 "~1.2.0"
+    exit-on-epipe "~1.0.1"
+    fflate "^0.3.8"
+    ssf "~0.11.2"
+    wmf "~1.0.1"
+    word "~0.3.0"
+
 xlsx@^0.18.5:
   version "0.18.5"
   resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.18.5.tgz#16711b9113c848076b8a177022799ad356eba7d0"