5 Commits d84850e489 ... 91ce720870

Auteur SHA1 Message Date
  code4eat 91ce720870 Merge branch 'feature/project-improvement' into master il y a 1 mois
  code4eat ad27f2e8c5 feat: 新增服务评价管理功能 il y a 1 mois
  code4eat 6b3697af37 fix:分页重制搜索后的bug il y a 6 mois
  code4eat e6f37df15e fixed:切换bug il y a 6 mois
  code4eat edc61abdf7 fixed:调整组织切换,无默认值时的报错 il y a 6 mois
54 fichiers modifiés avec 3839 ajouts et 692 suppressions
  1. 35 0
      .eslintrc.js
  2. 8 2
      .prettierrc
  3. 188 1
      README.md
  4. 2 1
      config/config.ts
  5. 2 2
      config/proxy.ts
  6. 6 0
      coverage/clover.xml
  7. 1 0
      coverage/coverage-final.json
  8. 224 0
      coverage/lcov-report/base.css
  9. 87 0
      coverage/lcov-report/block-navigation.js
  10. BIN
      coverage/lcov-report/favicon.png
  11. 101 0
      coverage/lcov-report/index.html
  12. 1 0
      coverage/lcov-report/prettify.css
  13. 1 0
      coverage/lcov-report/prettify.js
  14. BIN
      coverage/lcov-report/sort-arrow-sprite.png
  15. 196 0
      coverage/lcov-report/sorter.js
  16. 0 0
      coverage/lcov.info
  17. 56 56
      package.json
  18. 1 3
      public/zhongtaiC.js
  19. 145 145
      src/app.tsx
  20. 2 1
      src/components/intelligenceBot/Chat/index.tsx
  21. 9 3
      src/components/intelligenceBot/index.tsx
  22. 5 4
      src/components/intelligenceBot/style.less
  23. 173 27
      src/components/kcTable/index.tsx
  24. 10 0
      src/components/kcTable/typing.d.ts
  25. 1 1
      src/components/topBar/index.tsx
  26. 16 2
      src/global.less
  27. 4 3
      src/global.tsx
  28. 10 9
      src/layouts/index.tsx
  29. 9 0
      src/pages/index/index.less
  30. 22 7
      src/pages/index/index.tsx
  31. 61 10
      src/pages/platform/_layout.tsx
  32. 34 12
      src/pages/platform/setting/dataFilling/fillingMana/index.tsx
  33. 52 10
      src/pages/platform/setting/departmentMana/index.tsx
  34. 8 9
      src/pages/platform/setting/embeddedDashboard/index.tsx
  35. 8 0
      src/pages/platform/setting/embeddedDashboard/style.less
  36. 32 16
      src/pages/platform/setting/hospManage/index.tsx
  37. 28 17
      src/pages/platform/setting/hospParamsMana/index.tsx
  38. 28 40
      src/pages/platform/setting/indicatoLagacy/index.tsx
  39. 50 0
      src/pages/platform/setting/indicatorMana/frameContainer/index.tsx
  40. 128 32
      src/pages/platform/setting/indicatorMana/index.tsx
  41. 54 37
      src/pages/platform/setting/indicatorMana/style.less
  42. 47 8
      src/pages/platform/setting/kcClassification/index.tsx
  43. 41 12
      src/pages/platform/setting/notificationTemplate/index.tsx
  44. 28 8
      src/pages/platform/setting/paramsMana/index.tsx
  45. 21 5
      src/pages/platform/setting/pubDicMana/index.tsx
  46. 22 6
      src/pages/platform/setting/pubDicTypeMana/index.tsx
  47. 133 54
      src/pages/platform/setting/roleManage/index.tsx
  48. 1052 0
      src/pages/platform/setting/serviceEvaluate/index.tsx
  49. 302 0
      src/pages/platform/setting/serviceEvaluate/service.ts
  50. 166 0
      src/pages/platform/setting/serviceEvaluate/style.less
  51. 18 5
      src/pages/platform/setting/systemNavMana/index.tsx
  52. 32 51
      src/pages/platform/setting/userManage/index.tsx
  53. 11 1
      src/service/indicator.ts
  54. 168 92
      src/utils.ts

+ 35 - 0
.eslintrc.js

@@ -0,0 +1,35 @@
+module.exports = {
+  extends: [
+    'eslint:recommended',
+    'plugin:@typescript-eslint/recommended',
+    'plugin:react/recommended',
+    'plugin:react-hooks/recommended',
+    'prettier'
+  ],
+  plugins: ['@typescript-eslint', 'react', 'react-hooks', 'prettier'],
+  env: {
+    browser: true,
+    es2021: true,
+    node: true
+  },
+  settings: {
+    react: {
+      version: 'detect'
+    }
+  },
+  parser: '@typescript-eslint/parser',
+  parserOptions: {
+    ecmaFeatures: {
+      jsx: true
+    },
+    ecmaVersion: 12,
+    sourceType: 'module'
+  },
+  rules: {
+    'prettier/prettier': 'error',
+    'react/react-in-jsx-scope': 'off',
+    '@typescript-eslint/explicit-module-boundary-types': 'off',
+    '@typescript-eslint/no-explicit-any': 'warn',
+    'react/prop-types': 'off'
+  }
+}; 

+ 8 - 2
.prettierrc

@@ -1,7 +1,13 @@
 {
+  "semi": true,
   "singleQuote": true,
-  "trailingComma": "all",
-  "printWidth": 200,
+  "trailingComma": "es5",
+  "printWidth": 100,
+  "tabWidth": 2,
+  "useTabs": false,
+  "bracketSpacing": true,
+  "arrowParens": "avoid",
+  "endOfLine": "lf",
   "overrides": [
     {
       "files": ".prettierrc",

+ 188 - 1
README.md

@@ -1,6 +1,193 @@
+# 医院中台系统
 
+![项目Logo](./public/projectlogo.jpg)
 
-![image-20211126105817314](./public/projectlogo.jpg)
+## 项目简介
+
+医院中台系统是一个基于 React 和 TypeScript 开发的现代化医疗管理平台。该系统采用微前端架构,提供统一的用户界面和业务功能。
+
+## 技术栈
+
+- React 18
+- TypeScript
+- UmiJS 3
+- Ant Design 4
+- Monaco Editor
+- 微前端架构(qiankun)
+
+## 开发环境要求
+
+- Node.js >= 14
+- Yarn >= 1.22
+- Git
+
+## 快速开始
+
+### 安装依赖
+
+```bash
+yarn install
+```
+
+### 开发环境运行
+
+```bash
+# 包含 mock 数据
+yarn start
+
+# 开发环境(使用真实接口)
+yarn start:dev
+```
+
+### 构建生产环境
+
+```bash
+yarn build
+```
+
+### 代码质量检查
+
+```bash
+# 运行 ESLint 检查
+yarn lint
+
+# 自动修复 ESLint 问题
+yarn lint:fix
+
+# 运行 Prettier 格式化
+yarn prettier
+```
+
+## 项目结构
+
+```
+.
+├── config/                 # 项目配置文件
+├── mock/                   # Mock 数据
+├── public/                 # 静态资源
+├── src/                    # 源代码
+│   ├── components/        # 公共组件
+│   ├── layouts/          # 布局组件
+│   ├── pages/            # 页面组件
+│   ├── services/         # API 服务
+│   └── utils/            # 工具函数
+├── .eslintrc.js          # ESLint 配置
+├── .prettierrc           # Prettier 配置
+├── package.json          # 项目依赖
+└── tsconfig.json         # TypeScript 配置
+```
+
+## 开发规范
+
+### 代码风格
+
+- 使用 ESLint 和 Prettier 进行代码格式化
+- 遵循 TypeScript 严格模式
+- 使用函数组件和 Hooks
+- 遵循 React 最佳实践
+
+### Git 提交规范
+
+提交信息格式:
+```
+<type>(<scope>): <subject>
+
+<body>
+
+<footer>
+```
+
+type 类型:
+- feat: 新功能
+- fix: 修复
+- docs: 文档
+- style: 格式
+- refactor: 重构
+- test: 测试
+- chore: 构建过程或辅助工具的变动
+
+### 分支管理
+
+- master: 主分支
+- dev: 开发分支
+- feature/*: 功能分支
+- hotfix/*: 紧急修复分支
+
+## 部署
+
+### 环境要求
+
+- Node.js >= 14
+- Nginx >= 1.18
+
+### 部署步骤
+
+1. 构建项目
+```bash
+yarn build
+```
+
+2. 配置 Nginx
+```nginx
+server {
+    listen 80;
+    server_name your-domain.com;
+
+    location / {
+        root /path/to/dist;
+        try_files $uri $uri/ /index.html;
+    }
+}
+```
+
+## 常见问题
+
+### 1. 安装依赖失败
+
+解决方案:
+```bash
+# 清除 yarn 缓存
+yarn cache clean
+
+# 重新安装依赖
+yarn install
+```
+
+### 2. 开发环境接口代理
+
+在 `config/proxy.ts` 中配置代理规则:
+```typescript
+export default {
+  dev: {
+    '/api/': {
+      target: 'http://your-api-server',
+      changeOrigin: true,
+      pathRewrite: { '^/api': '' },
+    },
+  },
+};
+```
+
+## 贡献指南
+
+1. Fork 项目
+2. 创建功能分支
+3. 提交变更
+4. 发起 Pull Request
+
+## 版本历史
+
+- v1.0.0 (2023-12-01)
+  - 初始版本发布
+  - 基础功能实现
+
+## 维护者
+
+- 开发团队
+
+## 许可证
+
+MIT License
 
 
 

+ 2 - 1
config/config.ts

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2022-01-07 10:04:20
- * @LastEditTime: 2025-03-27 11:12:39
+ * @LastEditTime: 2025-04-08 11:45:42
  * @LastEditors: code4eat awesomedema@gmail.com
  * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  * @FilePath: /KC-MiddlePlatform/config/config.ts
@@ -11,6 +11,7 @@ import { defineConfig } from 'umi';
 
 import proxy from './proxy';
 
+
 const { REACT_APP_ENV } = process.env;
 
 export default defineConfig({

+ 2 - 2
config/proxy.ts

@@ -2,7 +2,7 @@
  * @Author: code4eat awesomedema@gmail.com
  * @Date: 2024-04-09 18:07:34
  * @LastEditors: code4eat awesomedema@gmail.com
- * @LastEditTime: 2025-03-27 14:03:32
+ * @LastEditTime: 2025-05-21 17:30:57
  * @FilePath: /KC-MiddlePlatform/config/proxy.ts
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */
@@ -17,7 +17,7 @@ const proxy: { [key: string]: any } = {
       // pathRewrite: { '^/master': '' },
     },
     '/api': {
-      target: 'http://localhost:8088',
+      target: 'http://120.27.235.181:8088',
       changeOrigin: true,
     },
     '/ai': {

+ 6 - 0
coverage/clover.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<coverage generated="1745983496334" clover="3.2.0">
+  <project timestamp="1745983496334" name="All files">
+    <metrics statements="0" coveredstatements="0" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0" elements="0" coveredelements="0" complexity="0" loc="0" ncloc="0" packages="0" files="0" classes="0"/>
+  </project>
+</coverage>

+ 1 - 0
coverage/coverage-final.json

@@ -0,0 +1 @@
+{}

+ 224 - 0
coverage/lcov-report/base.css

@@ -0,0 +1,224 @@
+body, html {
+  margin:0; padding: 0;
+  height: 100%;
+}
+body {
+    font-family: Helvetica Neue, Helvetica, Arial;
+    font-size: 14px;
+    color:#333;
+}
+.small { font-size: 12px; }
+*, *:after, *:before {
+  -webkit-box-sizing:border-box;
+     -moz-box-sizing:border-box;
+          box-sizing:border-box;
+  }
+h1 { font-size: 20px; margin: 0;}
+h2 { font-size: 14px; }
+pre {
+    font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
+    margin: 0;
+    padding: 0;
+    -moz-tab-size: 2;
+    -o-tab-size:  2;
+    tab-size: 2;
+}
+a { color:#0074D9; text-decoration:none; }
+a:hover { text-decoration:underline; }
+.strong { font-weight: bold; }
+.space-top1 { padding: 10px 0 0 0; }
+.pad2y { padding: 20px 0; }
+.pad1y { padding: 10px 0; }
+.pad2x { padding: 0 20px; }
+.pad2 { padding: 20px; }
+.pad1 { padding: 10px; }
+.space-left2 { padding-left:55px; }
+.space-right2 { padding-right:20px; }
+.center { text-align:center; }
+.clearfix { display:block; }
+.clearfix:after {
+  content:'';
+  display:block;
+  height:0;
+  clear:both;
+  visibility:hidden;
+  }
+.fl { float: left; }
+@media only screen and (max-width:640px) {
+  .col3 { width:100%; max-width:100%; }
+  .hide-mobile { display:none!important; }
+}
+
+.quiet {
+  color: #7f7f7f;
+  color: rgba(0,0,0,0.5);
+}
+.quiet a { opacity: 0.7; }
+
+.fraction {
+  font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
+  font-size: 10px;
+  color: #555;
+  background: #E8E8E8;
+  padding: 4px 5px;
+  border-radius: 3px;
+  vertical-align: middle;
+}
+
+div.path a:link, div.path a:visited { color: #333; }
+table.coverage {
+  border-collapse: collapse;
+  margin: 10px 0 0 0;
+  padding: 0;
+}
+
+table.coverage td {
+  margin: 0;
+  padding: 0;
+  vertical-align: top;
+}
+table.coverage td.line-count {
+    text-align: right;
+    padding: 0 5px 0 20px;
+}
+table.coverage td.line-coverage {
+    text-align: right;
+    padding-right: 10px;
+    min-width:20px;
+}
+
+table.coverage td span.cline-any {
+    display: inline-block;
+    padding: 0 5px;
+    width: 100%;
+}
+.missing-if-branch {
+    display: inline-block;
+    margin-right: 5px;
+    border-radius: 3px;
+    position: relative;
+    padding: 0 4px;
+    background: #333;
+    color: yellow;
+}
+
+.skip-if-branch {
+    display: none;
+    margin-right: 10px;
+    position: relative;
+    padding: 0 4px;
+    background: #ccc;
+    color: white;
+}
+.missing-if-branch .typ, .skip-if-branch .typ {
+    color: inherit !important;
+}
+.coverage-summary {
+  border-collapse: collapse;
+  width: 100%;
+}
+.coverage-summary tr { border-bottom: 1px solid #bbb; }
+.keyline-all { border: 1px solid #ddd; }
+.coverage-summary td, .coverage-summary th { padding: 10px; }
+.coverage-summary tbody { border: 1px solid #bbb; }
+.coverage-summary td { border-right: 1px solid #bbb; }
+.coverage-summary td:last-child { border-right: none; }
+.coverage-summary th {
+  text-align: left;
+  font-weight: normal;
+  white-space: nowrap;
+}
+.coverage-summary th.file { border-right: none !important; }
+.coverage-summary th.pct { }
+.coverage-summary th.pic,
+.coverage-summary th.abs,
+.coverage-summary td.pct,
+.coverage-summary td.abs { text-align: right; }
+.coverage-summary td.file { white-space: nowrap;  }
+.coverage-summary td.pic { min-width: 120px !important;  }
+.coverage-summary tfoot td { }
+
+.coverage-summary .sorter {
+    height: 10px;
+    width: 7px;
+    display: inline-block;
+    margin-left: 0.5em;
+    background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
+}
+.coverage-summary .sorted .sorter {
+    background-position: 0 -20px;
+}
+.coverage-summary .sorted-desc .sorter {
+    background-position: 0 -10px;
+}
+.status-line {  height: 10px; }
+/* yellow */
+.cbranch-no { background: yellow !important; color: #111; }
+/* dark red */
+.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
+.low .chart { border:1px solid #C21F39 }
+.highlighted,
+.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
+  background: #C21F39 !important;
+}
+/* medium red */
+.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
+/* light red */
+.low, .cline-no { background:#FCE1E5 }
+/* light green */
+.high, .cline-yes { background:rgb(230,245,208) }
+/* medium green */
+.cstat-yes { background:rgb(161,215,106) }
+/* dark green */
+.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
+.high .chart { border:1px solid rgb(77,146,33) }
+/* dark yellow (gold) */
+.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
+.medium .chart { border:1px solid #f9cd0b; }
+/* light yellow */
+.medium { background: #fff4c2; }
+
+.cstat-skip { background: #ddd; color: #111; }
+.fstat-skip { background: #ddd; color: #111 !important; }
+.cbranch-skip { background: #ddd !important; color: #111; }
+
+span.cline-neutral { background: #eaeaea; }
+
+.coverage-summary td.empty {
+    opacity: .5;
+    padding-top: 4px;
+    padding-bottom: 4px;
+    line-height: 1;
+    color: #888;
+}
+
+.cover-fill, .cover-empty {
+  display:inline-block;
+  height: 12px;
+}
+.chart {
+  line-height: 0;
+}
+.cover-empty {
+    background: white;
+}
+.cover-full {
+    border-right: none !important;
+}
+pre.prettyprint {
+    border: none !important;
+    padding: 0 !important;
+    margin: 0 !important;
+}
+.com { color: #999 !important; }
+.ignore-none { color: #999; font-weight: normal; }
+
+.wrapper {
+  min-height: 100%;
+  height: auto !important;
+  height: 100%;
+  margin: 0 auto -48px;
+}
+.footer, .push {
+  height: 48px;
+}

+ 87 - 0
coverage/lcov-report/block-navigation.js

@@ -0,0 +1,87 @@
+/* eslint-disable */
+var jumpToCode = (function init() {
+    // Classes of code we would like to highlight in the file view
+    var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
+
+    // Elements to highlight in the file listing view
+    var fileListingElements = ['td.pct.low'];
+
+    // We don't want to select elements that are direct descendants of another match
+    var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
+
+    // Selecter that finds elements on the page to which we can jump
+    var selector =
+        fileListingElements.join(', ') +
+        ', ' +
+        notSelector +
+        missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
+
+    // The NodeList of matching elements
+    var missingCoverageElements = document.querySelectorAll(selector);
+
+    var currentIndex;
+
+    function toggleClass(index) {
+        missingCoverageElements
+            .item(currentIndex)
+            .classList.remove('highlighted');
+        missingCoverageElements.item(index).classList.add('highlighted');
+    }
+
+    function makeCurrent(index) {
+        toggleClass(index);
+        currentIndex = index;
+        missingCoverageElements.item(index).scrollIntoView({
+            behavior: 'smooth',
+            block: 'center',
+            inline: 'center'
+        });
+    }
+
+    function goToPrevious() {
+        var nextIndex = 0;
+        if (typeof currentIndex !== 'number' || currentIndex === 0) {
+            nextIndex = missingCoverageElements.length - 1;
+        } else if (missingCoverageElements.length > 1) {
+            nextIndex = currentIndex - 1;
+        }
+
+        makeCurrent(nextIndex);
+    }
+
+    function goToNext() {
+        var nextIndex = 0;
+
+        if (
+            typeof currentIndex === 'number' &&
+            currentIndex < missingCoverageElements.length - 1
+        ) {
+            nextIndex = currentIndex + 1;
+        }
+
+        makeCurrent(nextIndex);
+    }
+
+    return function jump(event) {
+        if (
+            document.getElementById('fileSearch') === document.activeElement &&
+            document.activeElement != null
+        ) {
+            // if we're currently focused on the search input, we don't want to navigate
+            return;
+        }
+
+        switch (event.which) {
+            case 78: // n
+            case 74: // j
+                goToNext();
+                break;
+            case 66: // b
+            case 75: // k
+            case 80: // p
+                goToPrevious();
+                break;
+        }
+    };
+})();
+window.addEventListener('keydown', jumpToCode);

BIN
coverage/lcov-report/favicon.png


+ 101 - 0
coverage/lcov-report/index.html

@@ -0,0 +1,101 @@
+
+<!doctype html>
+<html lang="en">
+
+<head>
+    <title>Code coverage report for All files</title>
+    <meta charset="utf-8" />
+    <link rel="stylesheet" href="prettify.css" />
+    <link rel="stylesheet" href="base.css" />
+    <link rel="shortcut icon" type="image/x-icon" href="favicon.png" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <style type='text/css'>
+        .coverage-summary .sorter {
+            background-image: url(sort-arrow-sprite.png);
+        }
+    </style>
+</head>
+    
+<body>
+<div class='wrapper'>
+    <div class='pad1'>
+        <h1>All files</h1>
+        <div class='clearfix'>
+            
+            <div class='fl pad1y space-right2'>
+                <span class="strong">Unknown% </span>
+                <span class="quiet">Statements</span>
+                <span class='fraction'>0/0</span>
+            </div>
+        
+            
+            <div class='fl pad1y space-right2'>
+                <span class="strong">Unknown% </span>
+                <span class="quiet">Branches</span>
+                <span class='fraction'>0/0</span>
+            </div>
+        
+            
+            <div class='fl pad1y space-right2'>
+                <span class="strong">Unknown% </span>
+                <span class="quiet">Functions</span>
+                <span class='fraction'>0/0</span>
+            </div>
+        
+            
+            <div class='fl pad1y space-right2'>
+                <span class="strong">Unknown% </span>
+                <span class="quiet">Lines</span>
+                <span class='fraction'>0/0</span>
+            </div>
+        
+            
+        </div>
+        <p class="quiet">
+            Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
+        </p>
+        <template id="filterTemplate">
+            <div class="quiet">
+                Filter:
+                <input type="search" id="fileSearch">
+            </div>
+        </template>
+    </div>
+    <div class='status-line medium'></div>
+    <div class="pad1">
+<table class="coverage-summary">
+<thead>
+<tr>
+   <th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
+   <th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
+   <th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
+   <th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
+   <th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
+   <th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
+   <th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
+   <th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
+   <th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
+   <th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
+</tr>
+</thead>
+<tbody></tbody>
+</table>
+</div>
+                <div class='push'></div><!-- for sticky footer -->
+            </div><!-- /wrapper -->
+            <div class='footer quiet pad2 space-top1 center small'>
+                Code coverage generated by
+                <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
+                at 2025-04-30T03:24:56.319Z
+            </div>
+        <script src="prettify.js"></script>
+        <script>
+            window.onload = function () {
+                prettyPrint();
+            };
+        </script>
+        <script src="sorter.js"></script>
+        <script src="block-navigation.js"></script>
+    </body>
+</html>
+    

+ 1 - 0
coverage/lcov-report/prettify.css

@@ -0,0 +1 @@
+.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}

Fichier diff supprimé car celui-ci est trop grand
+ 1 - 0
coverage/lcov-report/prettify.js


BIN
coverage/lcov-report/sort-arrow-sprite.png


+ 196 - 0
coverage/lcov-report/sorter.js

@@ -0,0 +1,196 @@
+/* eslint-disable */
+var addSorting = (function() {
+    'use strict';
+    var cols,
+        currentSort = {
+            index: 0,
+            desc: false
+        };
+
+    // returns the summary table element
+    function getTable() {
+        return document.querySelector('.coverage-summary');
+    }
+    // returns the thead element of the summary table
+    function getTableHeader() {
+        return getTable().querySelector('thead tr');
+    }
+    // returns the tbody element of the summary table
+    function getTableBody() {
+        return getTable().querySelector('tbody');
+    }
+    // returns the th element for nth column
+    function getNthColumn(n) {
+        return getTableHeader().querySelectorAll('th')[n];
+    }
+
+    function onFilterInput() {
+        const searchValue = document.getElementById('fileSearch').value;
+        const rows = document.getElementsByTagName('tbody')[0].children;
+        for (let i = 0; i < rows.length; i++) {
+            const row = rows[i];
+            if (
+                row.textContent
+                    .toLowerCase()
+                    .includes(searchValue.toLowerCase())
+            ) {
+                row.style.display = '';
+            } else {
+                row.style.display = 'none';
+            }
+        }
+    }
+
+    // loads the search box
+    function addSearchBox() {
+        var template = document.getElementById('filterTemplate');
+        var templateClone = template.content.cloneNode(true);
+        templateClone.getElementById('fileSearch').oninput = onFilterInput;
+        template.parentElement.appendChild(templateClone);
+    }
+
+    // loads all columns
+    function loadColumns() {
+        var colNodes = getTableHeader().querySelectorAll('th'),
+            colNode,
+            cols = [],
+            col,
+            i;
+
+        for (i = 0; i < colNodes.length; i += 1) {
+            colNode = colNodes[i];
+            col = {
+                key: colNode.getAttribute('data-col'),
+                sortable: !colNode.getAttribute('data-nosort'),
+                type: colNode.getAttribute('data-type') || 'string'
+            };
+            cols.push(col);
+            if (col.sortable) {
+                col.defaultDescSort = col.type === 'number';
+                colNode.innerHTML =
+                    colNode.innerHTML + '<span class="sorter"></span>';
+            }
+        }
+        return cols;
+    }
+    // attaches a data attribute to every tr element with an object
+    // of data values keyed by column name
+    function loadRowData(tableRow) {
+        var tableCols = tableRow.querySelectorAll('td'),
+            colNode,
+            col,
+            data = {},
+            i,
+            val;
+        for (i = 0; i < tableCols.length; i += 1) {
+            colNode = tableCols[i];
+            col = cols[i];
+            val = colNode.getAttribute('data-value');
+            if (col.type === 'number') {
+                val = Number(val);
+            }
+            data[col.key] = val;
+        }
+        return data;
+    }
+    // loads all row data
+    function loadData() {
+        var rows = getTableBody().querySelectorAll('tr'),
+            i;
+
+        for (i = 0; i < rows.length; i += 1) {
+            rows[i].data = loadRowData(rows[i]);
+        }
+    }
+    // sorts the table using the data for the ith column
+    function sortByIndex(index, desc) {
+        var key = cols[index].key,
+            sorter = function(a, b) {
+                a = a.data[key];
+                b = b.data[key];
+                return a < b ? -1 : a > b ? 1 : 0;
+            },
+            finalSorter = sorter,
+            tableBody = document.querySelector('.coverage-summary tbody'),
+            rowNodes = tableBody.querySelectorAll('tr'),
+            rows = [],
+            i;
+
+        if (desc) {
+            finalSorter = function(a, b) {
+                return -1 * sorter(a, b);
+            };
+        }
+
+        for (i = 0; i < rowNodes.length; i += 1) {
+            rows.push(rowNodes[i]);
+            tableBody.removeChild(rowNodes[i]);
+        }
+
+        rows.sort(finalSorter);
+
+        for (i = 0; i < rows.length; i += 1) {
+            tableBody.appendChild(rows[i]);
+        }
+    }
+    // removes sort indicators for current column being sorted
+    function removeSortIndicators() {
+        var col = getNthColumn(currentSort.index),
+            cls = col.className;
+
+        cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
+        col.className = cls;
+    }
+    // adds sort indicators for current column being sorted
+    function addSortIndicators() {
+        getNthColumn(currentSort.index).className += currentSort.desc
+            ? ' sorted-desc'
+            : ' sorted';
+    }
+    // adds event listeners for all sorter widgets
+    function enableUI() {
+        var i,
+            el,
+            ithSorter = function ithSorter(i) {
+                var col = cols[i];
+
+                return function() {
+                    var desc = col.defaultDescSort;
+
+                    if (currentSort.index === i) {
+                        desc = !currentSort.desc;
+                    }
+                    sortByIndex(i, desc);
+                    removeSortIndicators();
+                    currentSort.index = i;
+                    currentSort.desc = desc;
+                    addSortIndicators();
+                };
+            };
+        for (i = 0; i < cols.length; i += 1) {
+            if (cols[i].sortable) {
+                // add the click event handler on the th so users
+                // dont have to click on those tiny arrows
+                el = getNthColumn(i).querySelector('.sorter').parentElement;
+                if (el.addEventListener) {
+                    el.addEventListener('click', ithSorter(i));
+                } else {
+                    el.attachEvent('onclick', ithSorter(i));
+                }
+            }
+        }
+    }
+    // adds sorting functionality to the UI
+    return function() {
+        if (!getTable()) {
+            return;
+        }
+        cols = loadColumns();
+        loadData();
+        addSearchBox();
+        addSortIndicators();
+        enableUI();
+    };
+})();
+
+window.addEventListener('load', addSorting);

+ 0 - 0
coverage/lcov.info


+ 56 - 56
package.json

@@ -1,57 +1,57 @@
 {
-  "private": true,
-  "scripts": {
-    "start": "umi dev",
-    "start:dev": "cross-env REACT_APP_ENV=dev UMI_ENV=dev umi dev",
-    "build": "umi build",
-    "postinstall": "umi generate tmp",
-    "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
-    "test": "umi-test",
-    "test:coverage": "umi-test --coverage"
-  },
-  "gitHooks": {
-    "pre-commit": "lint-staged"
-  },
-  "lint-staged": {
-    "*.{js,jsx,less,md,json}": [
-      "prettier --write"
-    ],
-    "*.ts?(x)": [
-      "prettier --parser=typescript --write"
-    ]
-  },
-  "dependencies": {
-    "@ant-design/charts": "^1.2.13",
-    "@ant-design/icons": "^4.5.0",
-    "@ant-design/pro-card": "^1.14.17",
-    "@ant-design/pro-descriptions": "^1.6.8",
-    "@ant-design/pro-form": "^1.70.3",
-    "@ant-design/pro-layout": "^6.38.13",
-    "@ant-design/pro-table": "^2.30.8",
-    "@monaco-editor/react": "^4.4.5",
-    "@superset-ui/embedded-sdk": "^0.1.3",
-    "antd": "^4.21.6",
-    "axios": "^1.3.4",
-    "cross-env": "^7.0.3",
-    "lodash": "^4.17.21",
-    "password-quality-calculator": "^1.0.4",
-    "react": "16.x",
-    "react-dev-inspector": "^1.1.1",
-    "react-dom": "16.x",
-    "react-markdown": "^8.0.0",
-    "umi": "^3.5.20"
-  },
-  "devDependencies": {
-    "@types/express": "^4.17.13",
-    "@types/react": "^16.0.0",
-    "@types/react-dom": "^16.0.0",
-    "@umijs/plugin-qiankun": "^2.35.4",
-    "@umijs/preset-react": "^2.1.1",
-    "@umijs/test": "^3.5.20",
-    "express": "^4.17.1",
-    "lint-staged": "^10.0.7",
-    "prettier": "^2.2.0",
-    "typescript": "^4.1.2",
-    "yorkie": "^2.0.0"
-  }
-}
+    "private": true,
+    "scripts": {
+      "start": "umi dev",
+      "start:dev": "cross-env REACT_APP_ENV=dev UMI_ENV=dev umi dev",
+      "build": "umi build",
+      "postinstall": "umi generate tmp",
+      "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
+      "test": "umi-test",
+      "test:coverage": "umi-test --coverage"
+    },
+    "gitHooks": {
+      "pre-commit": "lint-staged"
+    },
+    "lint-staged": {
+      "*.{js,jsx,less,md,json}": [
+        "prettier --write"
+      ],
+      "*.ts?(x)": [
+        "prettier --parser=typescript --write"
+      ]
+    },
+    "dependencies": {
+      "@ant-design/charts": "^1.2.13",
+      "@ant-design/icons": "^4.5.0",
+      "@ant-design/pro-card": "^1.14.17",
+      "@ant-design/pro-descriptions": "^1.6.8",
+      "@ant-design/pro-form": "^1.70.3",
+      "@ant-design/pro-layout": "^6.38.13",
+      "@ant-design/pro-table": "^2.30.8",
+      "@monaco-editor/react": "^4.4.5",
+      "@superset-ui/embedded-sdk": "^0.1.3",
+      "antd": "^4.21.6",
+      "axios": "^1.3.4",
+      "cross-env": "^7.0.3",
+      "lodash": "^4.17.21",
+      "password-quality-calculator": "^1.0.4",
+      "react": "16.x",
+      "react-dev-inspector": "^1.1.1",
+      "react-dom": "16.x",
+      "react-markdown": "^8.0.0",
+      "umi": "^3.5.20"
+    },
+    "devDependencies": {
+      "@types/express": "^4.17.13",
+      "@types/react": "^16.0.0",
+      "@types/react-dom": "^16.0.0",
+      "@umijs/plugin-qiankun": "^2.35.4",
+      "@umijs/preset-react": "^2.1.1",
+      "@umijs/test": "^3.5.20",
+      "express": "^4.17.1",
+      "lint-staged": "^10.0.7",
+      "prettier": "^2.2.0",
+      "typescript": "^4.1.2",
+      "yorkie": "^2.0.0"
+    }
+  }

Fichier diff supprimé car celui-ci est trop grand
+ 1 - 3
public/zhongtaiC.js


+ 145 - 145
src/app.tsx

@@ -5,176 +5,175 @@ import { RequestConfig, history } from 'umi';
 import type { RequestOptionsInit } from 'umi-request';
 import { UserDataType, getQiankunMicroApps } from '@/service/login';
 
-import { BasicLayoutProps } from '@ant-design/pro-layout';
 import { logoutHandle } from './global';
 import { KcimCenterSysId, Platforms } from './constant';
 import { SpacicalPageParamsType } from './typings';
 import { createFromIconfontCN } from '@ant-design/icons';
 import { changeFavicon, getHospType, handleLogin } from './pages/login';
 import { getSysParamsByCode } from './service';
-import '../public/zhongtaiC';
 
-window.isParentApp = true;
+// 全局变量声明
+declare global {
+  interface Window {
+    isParentApp?: boolean;
+  }
+}
 
 const loginPath = '/login';
 
-let hospSign: string = ''; //医院标识
-
-if (history && history.location.query) {
-  hospSign = history.location.query.hospSign as string;
-  if (!hospSign) {
-    const localHospSign = localStorage.getItem('hospSign');
-    hospSign = localHospSign ? localHospSign : '';
+// 获取医院标识
+const getHospSign = (): string => {
+  if (history?.location.query) {
+    const queryHospSign = history.location.query.hospSign as string;
+    if (queryHospSign) return queryHospSign;
   }
-}
+  return localStorage.getItem('hospSign') || '';
+};
+
+const hospSign = getHospSign();
 
 const IconFont = createFromIconfontCN({
-  scriptUrl: '',
-});
+  scriptUrl: '/zhongtaiC.js',
+}); 
 
 /** 获取用户信息比较慢的时候会展示一个 loading */
 export const initialStateConfig = {
   loading: <PageLoading />,
 };
 
-type InitialStateType = {
+interface InitialStateType {
   navData: any[];
-  menuData: any[]; //中台菜单
-  customerType: string; //客户类型
-  userData?: UserDataType; //登录返回用户信息
-  userInfo: any; //当前用户详细信息
-  systemLists?: TopBar.Tab[]; //当前医院可选子系统列表
-  openedSysLists?: TopBar.Tab[]; //当前已打开的系统列表
-  currentSelectedSys?: TopBar.Tab; //当前选中的tab
+  menuData: any[]; // 中台菜单
+  customerType: string; // 客户类型
+  userData?: UserDataType; // 登录返回用户信息
+  userInfo: any; // 当前用户详细信息
+  systemLists?: TopBar.Tab[]; // 当前医院可选子系统列表
+  openedSysLists?: TopBar.Tab[]; // 当前已打开的系统列表
+  currentSelectedSys?: TopBar.Tab; // 当前选中的tab
   logout?: () => Promise<boolean>;
   childAppIsShowMenu?: boolean;
   pageTitle: string;
   spacicalPageParamsType?: SpacicalPageParamsType[];
   getHospSubSystemListFunc?: () => Promise<any[]>;
-};
+}
 
 interface MyRequestOptions extends RequestOptionsInit {
   prefix?: string; // 自定义前缀
 }
 
-export async function getInitialState(): Promise<InitialStateType> {
-  const fetchUserInfo = async () => {
-    try {
-      const queryParams = new URLSearchParams(location.search);
-      const importUserData = queryParams.get('userData');
-      const env = queryParams.get('env');
-
-      if (importUserData) {
-        localStorage.setItem('userData', importUserData);
-        return JSON.parse(importUserData);
-      }
+/**
+ * 获取用户信息
+ */
+const fetchUserInfo = async (): Promise<UserDataType | undefined> => {
+  try {
+    const queryParams = new URLSearchParams(location.search);
+    const importUserData = queryParams.get('userData');
+    const env = queryParams.get('env');
 
-      const userData = localStorage.getItem('userData');
+    if (importUserData) {
+      localStorage.setItem('userData', importUserData);
+      return JSON.parse(importUserData);
+    }
 
-      if (userData) {
-        if (env === 'demo') {
-          history.replace('/index');
-        }
-        return JSON.parse(userData);
-      }
-      // 如果没有 userData 且 env 参数为 demo,则发起一个请求
+    const userData = localStorage.getItem('userData');
+    if (userData) {
       if (env === 'demo') {
-        // 获取 hospSign 参数
-
-        const hospSign = queryParams.get('hospSign');
-        // 发起请求的代码,根据实际需求调整
-        const resp = await handleLogin('demo', '123456', hospSign as string);
-        if (resp) {
-          history.replace('/index');
-          return resp;
-        }
+        history.replace('/index');
       }
+      return JSON.parse(userData);
+    }
 
-      throw Error;
-    } catch (error) {
-      const currentUrlParams = new URLSearchParams(window.location.search);
-      const hospSign = 'yourDefaultHospSign'; // 定义默认的 hospSign,如果需要的话
-
-      // 如果 hospSign 已存在,保留其值,否则添加新的 hospSign 参数
-      if (!currentUrlParams.has('hospSign')) {
-        currentUrlParams.append('hospSign', hospSign);
+    if (env === 'demo') {
+      const hospSign = queryParams.get('hospSign');
+      const resp = await handleLogin('demo', '123456', hospSign || '');
+      if (resp) {
+        history.replace('/index');
+        return resp;
       }
+    }
+
+    throw new Error('No user data found');
+  } catch (error) {
+    const currentUrlParams = new URLSearchParams(window.location.search);
+    const defaultHospSign = 'yourDefaultHospSign';
 
-      history.push(`${loginPath}?${currentUrlParams.toString()}`);
+    if (!currentUrlParams.has('hospSign')) {
+      currentUrlParams.append('hospSign', defaultHospSign);
     }
+
+    history.push(`${loginPath}?${currentUrlParams.toString()}`);
     return undefined;
-  };
+  }
+};
 
-  //获取当前账号所有子应用列表
-  const getHospSubSystemListFunc = async () => {
-    // const data = await getHospSubSystemList();
-    // if (data) {
-    //   const _data = data.map((t) => ({
-    //     ...t,
-    //     icon: getAppIcon(t.name),
-    //     path: t.path,
-    //   }));
-
-    //   return _data;
-    // }
-    return [];
-  };
+/**
+ * 获取医院子系统列表
+ */
+const getHospSubSystemListFunc = async (): Promise<any[]> => {
+  // TODO: 实现获取子系统列表的逻辑
+  return [];
+};
 
+export async function getInitialState(): Promise<InitialStateType> {
   const logout = logoutHandle;
   const userData = await fetchUserInfo();
 
-  let customerType = undefined;
-
+  let customerType: string | undefined;
   if (userData?.token) {
     customerType = await getHospType();
   }
 
   let systemLists: userRelationInfo.OwnAppsItem[] = [];
-  // let navData:any[] = [];
   if (userData) {
     systemLists = await getHospSubSystemListFunc();
-    // navData = await getUserPlatformNav();
   }
 
   const localInitData = localStorage.getItem('initialState');
   const currentSelectedTab = localStorage.getItem('currentSelectedTab');
-  const { loginPic } = JSON.parse(localStorage.getItem('currentSelectedSubHop') as string);
-  const userInfo = JSON.parse(localStorage.getItem('userInfo') as string);
-
-  if (loginPic.length > 0) {
-    const arr = loginPic.split('|');
-    if (arr.length == 2) {
-      changeFavicon(arr[1]);
+  const currentSelectedSubHop = localStorage.getItem('currentSelectedSubHop');
+  const userInfo = localStorage.getItem('userInfo');
+
+  if (currentSelectedSubHop) {
+    const { loginPic } = JSON.parse(currentSelectedSubHop);
+    if (loginPic) {
+      const [_, favicon] = loginPic.split('|');
+      if (favicon) {
+        changeFavicon(favicon);
+      }
     }
   }
 
   return {
-    currentSelectedSys: currentSelectedTab != null ? JSON.parse(currentSelectedTab) : undefined,
+    currentSelectedSys: currentSelectedTab ? JSON.parse(currentSelectedTab) : undefined,
     openedSysLists: [],
-    ...JSON.parse(localInitData ? localInitData : '{}'), //覆盖,恢复tab状态
+    ...(localInitData ? JSON.parse(localInitData) : {}),
     userData,
     logout,
     pageTitle: '欢迎进入医管平台',
     favicon: '',
     customerType,
-    userInfo,
+    userInfo: userInfo ? JSON.parse(userInfo) : null,
     spacicalPageParamsType: [],
     getHospSubSystemListFunc,
-    systemLists: systemLists,
+    systemLists,
   };
 }
 
+/**
+ * 请求拦截器
+ */
 const requestInterceptorsHandle = (url: string, options: MyRequestOptions) => {
   const prefix = options.prefix || '/gateway';
   const userData = localStorage.getItem('userData');
-  let authHeader = { token: '' };
+  const authHeader = { token: '' };
 
   if (userData) {
     const { token } = JSON.parse(userData);
-    if (url.indexOf('/centerSys/route/list') == -1) {
+    if (!url.includes('/centerSys/route/list')) {
       authHeader.token = token;
     }
   }
+
   const finalUrl = url.startsWith(prefix) ? url : `${prefix}${url}`;
 
   return {
@@ -183,25 +182,19 @@ const requestInterceptorsHandle = (url: string, options: MyRequestOptions) => {
   };
 };
 
+/**
+ * 响应拦截器
+ */
 const responseInterceptorsHandle = async (response: Response, options: RequestOptionsInit) => {
   const { url, method } = options;
-  const _response: {
-    errorMessage: any;
-    message: any;
-    data?: any;
-    status: number;
-    errorCode?: number;
-    success?: boolean;
-    msg?: string;
-  } = await response.clone().json();
-
-  if (_response.errorCode == 401) {
+  const _response = await response.clone().json();
+
+  if (_response.errorCode === 401) {
     Modal.confirm({
       title: '抱歉,你的登录已过期,请重新登录!',
       okText: '确定',
       cancelText: '取消',
       maskClosable: false,
-      // cancelButtonProps:
       onOk: () => {
         logoutHandle();
         return Promise.resolve(true);
@@ -210,41 +203,37 @@ const responseInterceptorsHandle = async (response: Response, options: RequestOp
     return;
   }
 
-  if (_response.status == 200) {
-    if (url != '/centerSys/menu/checkKeygen' && method == 'POST') {
+  if (_response.status === 200) {
+    if (url !== '/centerSys/menu/checkKeygen' && method === 'POST') {
       message.success({
-        content: `操作成功!`,
+        content: '操作成功!',
         duration: 1,
       });
     }
 
-    if (_response.data != null || _response.data != undefined) {
-      return _response.data;
-    } else {
-      return true;
-    }
-  } else {
-    if ((_response.msg && _response.msg.indexOf('Token') != -1) || (_response.msg && _response.msg.indexOf('token') != -1)) {
-      Modal.confirm({
-        title: '抱歉,你的登录已过期,请重新登录!',
-        okText: '确定',
-        cancelText: '取消',
-        maskClosable: false,
-        // cancelButtonProps:
-        onOk: () => {
-          logoutHandle();
-          return Promise.resolve(true);
-        },
-      });
-      return;
-    } else {
-      notification.error({
-        message: '错误:',
-        description: `${_response.msg ? _response.msg : _response.message ? _response.message : _response.errorMessage}`,
-      });
-    }
-    return false;
+    return _response.data ?? true;
   }
+
+  const errorMessage = _response.msg || _response.message || _response.errorMessage;
+  if (errorMessage?.toLowerCase().includes('token')) {
+    Modal.confirm({
+      title: '抱歉,你的登录已过期,请重新登录!',
+      okText: '确定',
+      cancelText: '取消',
+      maskClosable: false,
+      onOk: () => {
+        logoutHandle();
+        return Promise.resolve(true);
+      },
+    });
+    return;
+  }
+
+  notification.error({
+    message: '错误:',
+    description: errorMessage,
+  });
+  return false;
 };
 
 interface ErrorInfoStructure {
@@ -356,10 +345,10 @@ export const qiankun = async () => {
         //   name: 'microApp', // 唯一 id
         //   entry: '//localhost:8808', // 开发
         // },
-        {
-          name: 'nursingWorkersManaSystem', // 唯一 id
-          entry: '//116.169.61.56:29528', //测试
-        },
+        // {
+        //   name: 'nursingWorkersManaSystem', // 唯一 id
+        //   entry: '//116.169.61.56:29528', //测试
+        // },
         // {
         //   name: 'reviewMana', // 唯一 id
         //   entry: '//120.27.235.181:5000/pfmview/',
@@ -368,20 +357,20 @@ export const qiankun = async () => {
         // },
         {
           name: 'budgetManaSystem', // 唯一 id
-          entry: '//localhost:8002',
-          //entry: '//120.27.235.181:5000/perform/',  //开发
+          //entry: '//localhost:8002',
+          entry: '//120.27.235.181:5000/perform/',  //开发
           //entry: '//47.96.149.190:5000/perform/', //演示
           //entry: '//198.198.203.161:5000/perform/', //淮南
         },
-        // {
-        //   name: 'pfmBackMana', // 唯一 id
-        //   entry: '//localhost:8001'
-        //   //entry: '//120.27.235.181:5000/pfmManager/', // 开发
-        // },
+        {
+          name: 'pfmBackMana', // 唯一 id
+          //entry: '//localhost:8001'
+          entry: '//120.27.235.181:5000/pfmManager/', // 开发
+        },
         {
           name: 'CostAccountingSys', // 唯一 id
-          //entry: '//localhost:8001',
-          entry: '//120.27.235.181:5000/costAccount/', // 开发
+          entry: '//localhost:8001',
+          //entry: '//120.27.235.181:5000/costAccount/', // 开发
         },
         {
           name: 'MediResourceManaSys', // 唯一 id
@@ -474,7 +463,18 @@ export function patchRoutes({ routes }: { routes: any }) {
   treeLoop(routes[0]);
 }
 
+// 定义布局组件的类型
+interface BasicLayoutProps {
+  headerRender?: boolean;
+  rightContentRender?: () => React.ReactNode;
+  footerRender?: () => React.ReactNode;
+  onPageChange?: () => void;
+  menuHeaderRender?: React.ReactNode;
+}
+
+// 布局组件配置
 export const layout = ({ initialState }: { initialState?: InitialStateType }): BasicLayoutProps => {
+  // 解构初始状态数据,设置默认值
   const { navData = [], menuData = [], customerType = '' } = initialState || {};
 
   return {

+ 2 - 1
src/components/intelligenceBot/Chat/index.tsx

@@ -85,6 +85,7 @@ interface BotMessageContentProps {
 }
 
 const BotMessageContent: React.FC<BotMessageContentProps> = React.memo(({ thinkContent, content, charts, onShowDetail, msg }) => {
+ 
   return (
     <div className="bot-message-text">
       <div className="think-text">{thinkContent}</div>
@@ -96,7 +97,7 @@ const BotMessageContent: React.FC<BotMessageContentProps> = React.memo(({ thinkC
               <IconFont type={'iconchakan'} style={{ fontSize: 16, paddingRight: 4 }} />
               详情
             </div>
-            {charts.type === 'LineChart' && <Line {...charts} />}
+            {(charts.type === 'LineChart' && charts) && <Line {...charts} />}
             {charts.type === 'ColumnChart' && <Column {...charts} />}
             {charts.type === 'PieChart' && <Pie {...charts} />}
           </div>

+ 9 - 3
src/components/intelligenceBot/index.tsx

@@ -65,6 +65,11 @@ const AiChat: React.FC = () => {
         body: JSON.stringify({ question: question }),
       });
 
+
+      if (!response.ok) {
+        throw new Error(`服务器错误: ${response.status} ${response.statusText}`);
+      }
+
       if (!response.body) {
         throw new Error('No response body');
       }
@@ -186,7 +191,7 @@ const AiChat: React.FC = () => {
         }
         return updated;
       });
-    } catch (error) {
+    } catch (error: any) {
       console.error('请求错误:', error);
       setMessages((prev) => {
         const updated = [...prev];
@@ -194,7 +199,7 @@ const AiChat: React.FC = () => {
         if (lastIndex >= 0 && updated[lastIndex].sender === 'bot') {
           updated[lastIndex] = {
             sender: 'bot',
-            content: '请求失败,请稍后重试。',
+            content: `请求失败: ${error.message || '未知错误,请稍后重试'}`,
             loading: false,
           };
         }
@@ -259,6 +264,7 @@ const AiChat: React.FC = () => {
     textArea.style.background = 'transparent';
     document.body.appendChild(textArea);
     textArea.select();
+    
     try {
       const successful = document.execCommand('copy');
       if (successful) console.log('复制成功');
@@ -304,7 +310,7 @@ const AiChat: React.FC = () => {
         onClose={closeDrawer}
         open={visible}
         destroyOnClose
-        width={detailData ? '90%' : 560}
+        width={detailData ? '94%' : 560}
         mask={false}
       >
         <div className="chat-header">

+ 5 - 4
src/components/intelligenceBot/style.less

@@ -135,9 +135,8 @@
           }
         }
         .chat-detail {
-          display: flex;
-          flex: 1;
-          width: 100%;
+          
+          width:calc(100% - 552px);
           height: 100%;
           background: #ffffff;
           border-radius: 8px;
@@ -181,8 +180,10 @@
               padding: 8px;
               padding-left: 18px;
               overflow-y: scroll;
+              overflow-x: scroll;
               .charts {
-                min-width: 640px;
+                width: 100%;
+                // min-width: 640px;
               }
             }
           }

+ 173 - 27
src/components/kcTable/index.tsx

@@ -7,19 +7,30 @@
  * @FilePath: /KC-MiddlePlatform/src/components/kcTable/index.tsx
  */
 
-import React, { useState, useEffect, useRef } from 'react';
+import React, { useState, useEffect, useRef, useMemo } from 'react';
 import ProTable, { ActionType, ProColumns } from '@ant-design/pro-table';
+import { message } from 'antd';
 import { KCTableType } from './typing';
 import './style.less';
+import { useDrag, useDrop } from 'react-dnd';
 
-const KCTable: React.FC<KCTableType.KCTableProps<any, {}>> = ({
+const KCTable: React.FC<KCTableType.KCTableProps<any, { current?: number; pageSize?: number; [key: string]: any }>> = ({
   columns,
   reload,
   newVer,
+  params,
+  request,
+  onChange,
+  dragSort,
+  dragSortKey,
+  dragType,
+  disabledDragKeys,
+  onDragSortEnd,
   ...restProps
 }) => {
   const [tableColumns, setTableColumns] = useState<ProColumns[]>([]);
   const actionRef = useRef<ActionType>();
+  const type = dragType || 'KC_TABLE_ROW'; // 使用自定义类型或默认类型
 
   useEffect(() => {
     setTableColumns(
@@ -36,38 +47,173 @@ const KCTable: React.FC<KCTableType.KCTableProps<any, {}>> = ({
     }
   }, [reload]);
 
+  // 处理分页变化
+  const handleTableChange = (pagination: any, filters: any, sorter: any, extra: any) => {
+    if (onChange) {
+      onChange(pagination, filters, sorter, extra);
+    }
+  };
+
+  const moveRow = React.useCallback((dragIndex: number, hoverIndex: number) => {
+    if (!onDragSortEnd) {
+      return;
+    }
+    const raw = (restProps as any).dataSource || [];
+    // 仅当存在有效数组时才排序,防止非对象触发下游渲染问题
+    if (!Array.isArray(raw) || raw.length === 0) {
+      return;
+    }
+    const data = raw.map((x: any) => (x && typeof x === 'object' ? { ...x } : x));
+    const newData = data.slice();
+    const dragRow = newData[dragIndex];
+    if (typeof dragRow !== 'object') {
+      return;
+    }
+    newData.splice(dragIndex, 1);
+    newData.splice(hoverIndex, 0, dragRow);
+    // 避免在 rc-table 渲染周期内立即变更数据导致内部校验异常,异步派发
+    setTimeout(() => onDragSortEnd(newData), 0);
+  }, [onDragSortEnd, restProps, dragSort]);
+
+  const components = useMemo(() => {
+    if (!dragSort) {
+      return undefined;
+    }
+    // 行组件:支持拖拽排序
+    const DragRow: React.FC<any> = ({ className, style, index, record, children, moveRow: moveRowProp, ...rest }) => {
+      const ref = useRef<HTMLTableRowElement>(null);
+      
+      // 获取行的 key,判断是否禁用拖拽
+      const rowKeyValue = dragSortKey ? record?.[dragSortKey] : record?.id;
+      const isDisabled = disabledDragKeys && disabledDragKeys.includes(rowKeyValue);
+
+      const [, drop] = useDrop({
+        accept: type,
+        hover: (item: any, monitor: any) => {
+          if (!ref.current) return;
+          if (isDisabled) return; // 禁用行不响应 hover
+          const dragIndex = item.index;
+          const hoverIndex = index;
+          if (dragIndex === hoverIndex) return;
+          const clientOffset = monitor.getClientOffset();
+          const hoverBoundingRect = ref.current.getBoundingClientRect();
+          const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
+          const hoverClientY = clientOffset ? clientOffset.y - hoverBoundingRect.top : 0;
+          if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return;
+          if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return;
+          // 使用从 props 传入的 moveRow
+          if (moveRowProp) {
+            moveRowProp(dragIndex, hoverIndex);
+          }
+          item.index = hoverIndex;
+        },
+        collect: (monitor) => ({
+          isOver: monitor.isOver(),
+        }),
+      });
+
+      const [{ isDragging }, drag] = useDrag({
+        type,
+        item: { index },
+        canDrag: () => {
+          if (isDisabled) {
+            message.warning('请先收起展开的行,再进行排序操作');
+            return false;
+          }
+          return true;
+        },
+        collect: (monitor) => ({ isDragging: monitor.isDragging() }),
+      });
+
+      // 所有行都应用拖拽,这样才能触发 canDrag 回调显示提示
+      drag(drop(ref));
+
+      return (
+        <tr 
+          ref={ref} 
+          className={className} 
+          style={{ 
+            opacity: isDragging ? 0.5 : 1, 
+            cursor: isDisabled ? 'not-allowed' : 'move',
+            ...style 
+          }} 
+          {...rest}
+        >
+          {children}
+        </tr>
+      );
+    };
+
+    return {
+      body: {
+        row: (props: any) => <DragRow {...props} />,
+      },
+    } as any;
+  }, [dragSort, type, disabledDragKeys, dragSortKey]);
+
   if(newVer){
     return (
-      <ProTable
-        tableStyle={{
-          border: '1px solid #DAE2F2',
-          borderRadius:4
-        }}
-        toolBarRender={false}
-        search={false}
-        className="KCTable"
-        actionRef={actionRef}
-        columns={tableColumns}
-        pagination={{ defaultPageSize: 10 }}
-        {...restProps}
-      />
+        <ProTable
+          tableStyle={{
+            border: '1px solid #DAE2F2',
+            borderRadius:4
+          }}
+          toolBarRender={false}
+          search={false}
+          className="KCTable"
+          actionRef={actionRef}
+          columns={tableColumns}
+          pagination={{
+            defaultPageSize: 10,
+            current: params?.current,
+            pageSize: params?.pageSize
+          }}
+          onChange={handleTableChange}
+          params={params}
+          request={request}
+          components={components}
+          onRow={(record: any, index) => {
+            if (!dragSort) return {} as any;
+            return {
+              index,
+              record,
+              moveRow,
+            } as any;
+          }}
+          {...restProps}
+        />
     );
   }else{
     return (
-      <ProTable
-        tableStyle={{
-          border: '1px solid #DAE2F2',
-          borderRadius:4
-        }}
-        className="KCTable"
-        actionRef={actionRef}
-        columns={tableColumns}
-        pagination={{ defaultPageSize: 10 }}
-        {...restProps}
-      />
+        <ProTable
+          tableStyle={{
+            border: '1px solid #DAE2F2',
+            borderRadius:4
+          }}
+          className="KCTable"
+          actionRef={actionRef}
+          columns={tableColumns}
+          pagination={{ 
+            defaultPageSize: 10,
+            current: params?.current,
+            pageSize: params?.pageSize
+          }}
+          onChange={handleTableChange}
+          params={params}
+          request={request}
+          components={components}
+          onRow={(record: any, index) => {
+            if (!dragSort) return {} as any;
+            return {
+              index,
+              record,
+              moveRow,
+            } as any;
+          }}
+          {...restProps}
+        />
     );
   }
-
 };
 
 export default KCTable;

+ 10 - 0
src/components/kcTable/typing.d.ts

@@ -18,5 +18,15 @@ declare namespace KCTableType {
     columns: ProColumns<any, 'text'>[];
     reload?: boolean;
     newVer?:boolean,
+    /** 是否开启行拖拽排序(左侧拖拽,整行作为拖拽源/目标) */
+    dragSort?: boolean;
+    /** 拖拽行的唯一键,默认使用 rowKey */
+    dragSortKey?: string;
+    /** 拖拽类型标识,用于区分不同层级的表格拖拽(避免嵌套表格拖拽冲突) */
+    dragType?: string;
+    /** 禁止拖拽的行键列表(例如已展开的行) */
+    disabledDragKeys?: React.Key[];
+    /** 拖拽结束回调,返回新顺序的数据(dataSource 模式下有效) */
+    onDragSortEnd?: (sortedRows: T[]) => void;
   }
 }

+ 1 - 1
src/components/topBar/index.tsx

@@ -483,7 +483,7 @@ const TopBar: React.FC<TopBarType> = (props) => {
           {showGroupList && (
             <div className="group-list-wrapper" ref={GroupListWrapperRef}>
               {groupList.map((a, index) => (
-                <div key={index} className={currentActivedGroup.orgId == a.orgId ? `group-list actived` : 'group-list'} onClick={() => switchGroupHandle(a)}>
+                <div key={index} className={currentActivedGroup?.orgId === a.orgId ? `group-list actived` : 'group-list'} onClick={() => switchGroupHandle(a)}>
                   {a.orgName}
                 </div>
               ))}

+ 16 - 2
src/global.less

@@ -9,6 +9,9 @@ body {
   overflow: hidden;
   zoom: unset !important;
   background-color: transparent;
+  &>#root-master {
+    height: 100%;
+  }
 }
 
 input {
@@ -20,9 +23,20 @@ input {
 }
 
 //overwrite antd-pro css
-
+.kcmp-ant-pro-page-container {
+  height: 100%;
+  .kcmp-ant-pro-grid-content {
+    height: 100%;
+    .kcmp-ant-pro-grid-content-children {
+      height: 100%;
+      .kcmp-ant-pro-page-container-children-content {
+        height: 100%;
+      }
+    }
+  }
+}
 .kcmp-ant-pro-page-container-warp {
-  display: none !important;
+  display: none !important;  
 }
 
 .kcmp-ant-pro-sider-light {

+ 4 - 3
src/global.tsx

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2021-12-21 11:05:37
- * @LastEditTime: 2024-03-28 18:04:34
+ * @LastEditTime: 2025-05-13 16:37:55
  * @LastEditors: code4eat awesomedema@gmail.com
  * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  * @FilePath: /KC-MiddlePlatform/src/global.tsx
@@ -21,8 +21,9 @@ export const logoutHandle = async () => {
   console.log('logoutHandle');
   localStorage.removeItem('userData');
   localStorage.removeItem('initialState');
-  // localStorage.removeItem('currentSelectedTab');
-  //localStorage.removeItem('isChildShowMenu');
+  localStorage.removeItem('userInfo');
+  localStorage.removeItem('currentSelectedTab');
+  localStorage.removeItem('isChildShowMenu');
 
   localStorage.removeItem('selectedKeys');
   localStorage.removeItem('openKeys');

+ 10 - 9
src/layouts/index.tsx

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2021-11-09 13:56:33
- * @LastEditTime: 2025-03-13 10:35:15
+ * @LastEditTime: 2025-04-27 13:43:23
  * @LastEditors: code4eat awesomedema@gmail.com
  * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  * @FilePath: /KC-MiddlePlatform/src/layouts/index.tsx
@@ -149,11 +149,8 @@ export default function Layout({ children, location, route, history, match }: IR
 
   const queryParams = new URLSearchParams(location.search);
   const noTopBar = queryParams.get('noTopbar');
-  const [ifStartChat, set_ifStartChat] = useState(false);
-  const [messages, setMessages] = useState<Message[]>([]);
-  const [inputValue, setInputValue] = useState<string>('');
-  const [loading, setLoading] = useState<boolean>(false);
-  const chatContainerRef = useRef<HTMLDivElement>(null);
+  const [ifShowAiBtn, set_ifShowAiBtn] = useState(false);
+
 
   const getNavData = async () => {
     const nav = await getUserPlatformNav();
@@ -173,6 +170,9 @@ export default function Layout({ children, location, route, history, match }: IR
         if (item.code == '1739123252502073344') {
           item.value && set_topBarTitle(item.value);
         }
+        if (item.code == '1907252043454746624') {
+          item.value && set_ifShowAiBtn(item.value == '1');
+        }
       });
     }
   };
@@ -182,6 +182,8 @@ export default function Layout({ children, location, route, history, match }: IR
     set_emptyPageContent(menuItem.description);
   };
 
+
+
   useEffect(() => {
     if (location.pathname != '/login') {
       getNavData();
@@ -231,7 +233,7 @@ export default function Layout({ children, location, route, history, match }: IR
         margin: 0,
       }}
       menuRender={false}
-      menuItemRender={(item, dom) => {
+      menuItemRender={(item, dom) => { 
         return (
           <a
             onClick={() => {
@@ -279,8 +281,7 @@ export default function Layout({ children, location, route, history, match }: IR
       )}
       {!isEmpty && (
         <div className="platform-children" style={{ height: noTopBar == 'true' ? 'auto' : `calc(100vh - 48px)`, minWidth: 1280 }}>
-          <AiChat />
-
+          {(window.self === window.top&&ifShowAiBtn)&&<AiChat />}
           <>{children}</>
         </div>
       )}

+ 9 - 0
src/pages/index/index.less

@@ -552,3 +552,12 @@
     }
   }
 }
+
+.departmentName {
+  display: inline-block; /* 内联块元素 */
+  max-width: 140px; /* 最大宽度,可根据实际调整 */
+  overflow: hidden; /* 超出隐藏 */
+  text-overflow: ellipsis; /* 超出显示省略号 */
+  white-space: nowrap; /* 不换行 */
+  vertical-align: bottom;
+}

+ 22 - 7
src/pages/index/index.tsx

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2021-11-10 09:33:30
- * @LastEditTime: 2025-03-13 14:12:10
+ * @LastEditTime: 2025-05-20 10:53:28
  * @LastEditors: code4eat awesomedema@gmail.com
  * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  * @FilePath: /KC-MiddlePlatform/src/pages/index.tsx
@@ -9,7 +9,7 @@
 
 import { useModel, history, Location, Helmet } from 'umi';
 import './index.less';
-import { Skeleton, Drawer } from 'antd';
+import { Skeleton, Drawer, Tooltip } from 'antd';
 import { useState, useEffect } from 'react';
 
 import { FastEntry, FastEntryTabType } from './components/FastEntry';
@@ -254,25 +254,40 @@ const IndexPage: React.FC<IndexPageType> = ({ location }) => {
                   <div className="label">
                     <img src={require('../../../public/images/icon-keshi.png')} alt="" />
                     <span>{customerType == '2' ? '组织' : '科室'}</span>
-                    {userInfo?.departmentName ? userInfo?.departmentName : '-'}
+                    <Tooltip title={userInfo?.departmentName}>
+                      <span className="departmentName">
+                        {userInfo?.departmentName ? userInfo?.departmentName : '-'}
+                      </span>
+                    </Tooltip>
                   </div>
                   <div className="label">
                     <img src={require('../../../public/images/icon-zhicheng.png')} alt="" />
                     <span>{customerType == '2' ? '职务' : '职称'}</span>
-                    {customerType == '2' && <>{userInfo?.jobTitle ?? '-'}</>}
-                    {customerType == '1' && <>{userInfo?.title ?? '-'}</>}
+                    <Tooltip title={customerType == '2' ? userInfo?.jobTitle : userInfo?.title}>
+                      <span className="departmentName">
+                        {customerType == '2' ? (userInfo?.jobTitle ?? '-') : (userInfo?.title ?? '-')}
+                      </span>
+                    </Tooltip>
                   </div>
                 </div>
                 <div className="row">
                   <div className="label">
                     <img src={require('../../../public/images/icon-leibie.png')} alt="" />
                     <span>类别</span>
-                    {userInfo?.userCate ? userInfo?.userCate : '-'}
+                    <Tooltip title={userInfo?.userCate}>
+                      <span className="departmentName">
+                        {userInfo?.userCate ? userInfo?.userCate : '-'}
+                      </span>
+                    </Tooltip>
                   </div>
                   <div className="label">
                     <img src={require('../../../public/images/icon-gangwei.png')} alt="" />
                     <span>岗位</span>
-                    {userInfo?.position ? userInfo?.position : '-'}
+                    <Tooltip title={userInfo?.position}>
+                      <span className="departmentName">
+                        {userInfo?.position ? userInfo?.position : '-'}
+                      </span>
+                    </Tooltip>
                   </div>
                 </div>
               </div>

+ 61 - 10
src/pages/platform/_layout.tsx

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2022-01-06 15:25:39
- * @LastEditTime: 2025-03-20 14:55:04
+ * @LastEditTime: 2025-05-22 14:30:28
  * @LastEditors: code4eat awesomedema@gmail.com
  * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  * @FilePath: /KC-MiddlePlatform/src/pages/platform/_layout.tsx
@@ -129,6 +129,7 @@ export default function Layout({ children, location, route, history, match, ...r
   const [emptyPageContent, set_emptyPageContent] = useState('');
   const [isShowPageMenu, set_isShowPageMenu] = useState(true);
   const [isEmpty, set_isEmpty] = useState(false);
+  const [isThirdPartySystem, setIsThirdPartySystem] = useState(false);
 
   const [collapsed, set_collapsed] = useState(false);
 
@@ -136,7 +137,7 @@ export default function Layout({ children, location, route, history, match, ...r
 
   const queryParams = new URLSearchParams(location.search);
   const noMenu = queryParams.get('noMenu');
-
+  const noTopBar = queryParams.get('noTopbar');
   const moreConfig: { menuRender?: any } = {};
 
   const { pathname } = location;
@@ -145,6 +146,9 @@ export default function Layout({ children, location, route, history, match, ...r
   const [loading, setLoading] = useState(true);
   const [dataLoaded, setDataLoaded] = useState(false);
 
+  // 用于记录上一次 isThirdPartySystem 状态
+  const prevIsThirdPartySystemRef = useRef(isThirdPartySystem);
+
   const setEmptyPageContent = async (menuId: Key) => {
     const menuItem = await getSpecifyMenuDetail(menuId);
     if (menuItem.isSetupMenu) {
@@ -166,7 +170,11 @@ export default function Layout({ children, location, route, history, match, ...r
 
   useEffect(() => {
     const isShowMenu = localStorage.getItem('isChildShowMenu');
-    set_isShowPageMenu(isShowMenu == 'true');
+    if (isShowMenu) {
+      set_isShowPageMenu(isShowMenu == 'true');
+    } else {
+      set_isShowPageMenu(true);
+    }
   });
 
   useEffect(() => {
@@ -333,10 +341,47 @@ export default function Layout({ children, location, route, history, match, ...r
     });
   }, [dataLoaded, initialState]);
 
+  useEffect(() => {
+    if (initialState?.currentSelectedSys) {
+      const { type, contentType } = initialState.currentSelectedSys;
+      setIsThirdPartySystem(type === 1 && contentType === 6);
+    }
+  }, [initialState?.currentSelectedSys]);
+
+  useEffect(() => {
+    // 只有从第三方系统切回中台时才处理
+    if (prevIsThirdPartySystemRef.current && !isThirdPartySystem) {
+      // 取当前选中的菜单 key
+      if (selectedKeys && selectedKeys.length > 0 && initialState?.menuData) {
+        // 递归查找菜单项
+        const findItemByKey = (tree: any[], key: string, keyName: string): any => {
+          for (const node of tree) {
+            if (node[`${keyName}`] === key) {
+              return node;
+            }
+            if (node.children) {
+              const result = findItemByKey(node.children, key, keyName);
+              if (result) {
+                return result;
+              }
+            }
+          }
+          return null;
+        };
+        const selectedMenu = findItemByKey(initialState.menuData, selectedKeys[0], 'key');
+        if (selectedMenu && selectedMenu.path && history.location.pathname !== selectedMenu.path) {
+          history.push(selectedMenu.path);
+        }
+      }
+    }
+    prevIsThirdPartySystemRef.current = isThirdPartySystem;
+  }, [isThirdPartySystem]);
+  
+
   return (
     <ProLayout
       style={{
-        height: 'calc(100vh - 50px)',
+        height:noTopBar?'100%': 'calc(100vh - 50px)',
       }}
       //iconfontUrl="//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js"
       logoStyle={{
@@ -392,8 +437,9 @@ export default function Layout({ children, location, route, history, match, ...r
           localStorage.setItem('openKeys', JSON.stringify(keys));
         },
       }}
-      menu={{
+      menu={isShowPageMenu && !noMenu ? {
         autoClose: false,
+        params:initialState?.currentSelectedSys,
         request: async () => {
           if (initialState && initialState.currentSelectedSys) {
             const { systemId, menuId, path, type, contentType } = initialState.currentSelectedSys;
@@ -521,7 +567,7 @@ export default function Layout({ children, location, route, history, match, ...r
           }
           return [];
         },
-      }}
+      } : undefined}
       menuRender={(props: any, defaultDom) => {
         return (
           <div style={isShowPageMenu && noMenu != 'true' ? {} : { display: 'inline-block', width: 0, overflow: 'hidden' }}>
@@ -562,7 +608,7 @@ export default function Layout({ children, location, route, history, match, ...r
       {...moreConfig}
     >
       {isEmpty && <EmptyPage textContent={emptyPageContent} />}
-      {!isEmpty && type != 1 && contentType != 6 && (
+      {!isEmpty && !isThirdPartySystem && (
         <PageContainer
           className="kcmpPageContainer"
           header={{
@@ -577,13 +623,18 @@ export default function Layout({ children, location, route, history, match, ...r
             <title>{initialState?.customerType == '2' ? currentHospName : '精益管理中台'}</title>
           </Helmet>
 
-          <div className="page" style={{ height: 'calc(100vh - 80px)', overflowY: 'scroll' }}>
+          <div className="page" style={noTopBar?{height:'100%',overflowY: 'scroll'}:{ height: 'calc(100vh - 80px)', overflowY: 'scroll' }}>
             {children}
           </div>
         </PageContainer>
       )}
-      {type == 1 && contentType == 6 && (
-        <iframe id={'bi_iframe'} style={{ width: '100%', height: '100%', border: 'none' }} src={addTokenToUrl(url as string, token)} onLoad={() => adjustIframe()}></iframe>
+      {isThirdPartySystem && (
+        <iframe 
+          id={'bi_iframe'} 
+          style={{ width: '100%', height: 'calc(100vh - 48px)', border: 'none' }} 
+          src={addTokenToUrl(url as string, token)} 
+          onLoad={() => adjustIframe()}
+        />
       )}
     </ProLayout>
   );

+ 34 - 12
src/pages/platform/setting/dataFilling/fillingMana/index.tsx

@@ -109,7 +109,14 @@ let total_tableDataSource: any[] = [];
 
 export default function FillingMana() {
   const { initialState, setInitialState } = useModel('@@initialState');
-  const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>();
+  const [tableDataFilterParams, set_tableDataFilterParams] = useState<{
+    current: number;
+    pageSize: number;
+    name?: string;
+  }>({
+    current: 1,
+    pageSize: 10
+  });
   const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
   const tableRef = useRef<ActionType>();
   const detailTableRef = useRef<ActionType>();
@@ -256,17 +263,24 @@ export default function FillingMana() {
     }
   };
   const getTableData = async (params: any) => {
-    const resp = await getTableList({ ...params });
+    const { current = 1, pageSize = 10, name } = params;
+    const resp = await getTableList({
+      current,
+      pageSize,
+      name
+    });
     if (resp) {
       return {
         data: resp.list,
         success: true,
-        total: resp.totalCount,
-        pageSize: resp.pageSize,
-        totalPage: resp.totalPage,
+        total: resp.totalCount
       };
     }
-    return [];
+    return {
+      data: [],
+      success: false,
+      total: 0
+    };
   };
 
   const delTableData = async (record: any) => {
@@ -314,13 +328,21 @@ export default function FillingMana() {
     return false;
   };
 
+  const handleTableChange = (pagination: any) => {
+    set_tableDataFilterParams({
+      ...tableDataFilterParams,
+      current: pagination.current,
+      pageSize: pagination.pageSize
+    });
+  }
+
   const tableDataSearchHandle = (paramName: string) => {
     set_tableDataFilterParams({
       ...tableDataFilterParams,
       current: 1,
-      [`${paramName}`]: tableDataSearchKeywords,
+      [`${paramName}`]: tableDataSearchKeywords
     });
-  };
+  }
 
   const detailtableDataSearchHandle = (paramName: string) => {
     const result = total_tableDataSource.filter((a: any) => a[`${paramName}`].indexOf(tableDataSearchKeywords) != -1);
@@ -637,13 +659,13 @@ export default function FillingMana() {
             </div>
           </div>
           <KCTable
-            newVer
-            columns={columns as ProColumns[]}
-            scroll={{ y: 'calc(100vh - 232px)' }}
-            actionRef={tableRef}
+            columns={columns}
+            scroll={{ y: `calc(100vh - 250px)` }}
             rowKey="id"
+            newVer
             params={tableDataFilterParams}
             request={(params) => getTableData(params)}
+            onChange={handleTableChange}
           />
         </div>
       )}

+ 52 - 10
src/pages/platform/setting/departmentMana/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-01-14 10:30:57
+ * @LastEditTime: 2025-05-13 16:51:44
  * @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
  */
@@ -14,7 +14,7 @@ import { getAllHosp } from '@/service/hospList';
 import { downloadTemplateReq } from '@/utils';
 import { ModalForm, ProFormSelect, ProFormText, ProFormTextArea } from '@ant-design/pro-form';
 import { ProColumns } from '@ant-design/pro-table';
-import { message, Modal, Popconfirm } from 'antd';
+import { Divider, message, Modal, Popconfirm, Tooltip } from 'antd';
 import FormItem from 'antd/lib/form/FormItem';
 import { useEffect, useState } from 'react';
 import { addData, delData, editData, getDepartmentData, getDepartmentType, getRelaHosp, importDepartmentData } from './service';
@@ -28,7 +28,14 @@ import { checkRoleTagsIsObjArr } from '../roleManage/modals/modal';
 
 export default function DepartmentMana() {
   const { initialState, setInitialState } = useModel('@@initialState');
-  const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>();
+  const [tableDataFilterParams, set_tableDataFilterParams] = useState<{
+    current: number;
+    pageSize: number;
+    name?: string;
+  }>({
+    current: 1,
+    pageSize: 10
+  });
   const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
   const [reload, set_reload] = useState(false);
   const [currentEdit, set_currentEdit] = useState<any>(undefined);
@@ -91,11 +98,29 @@ export default function DepartmentMana() {
       width: 120,
       valueType: 'option',
       render: (_: any, record: any) => {
+        const getEditBtn = () => {
+          const { isEdit } = record;
+          if (!isEdit) {
+            return [
+              <UpDataActBtn key={'act'} record={record} type="EDIT" />,
+              <Popconfirm title="是否确认删除?" key="del" onConfirm={() => delTableData(record)}>
+                <a>删除</a>
+              </Popconfirm>,
+            ];
+          } else {
+            return [
+              <Tooltip title="第三方科室不可编辑!">
+                <span style={{ cursor: 'not-allowed', color: 'rgb(176 181 189)' }}>编辑</span>
+              </Tooltip>,
+              <Divider key="1" type="vertical" style={{ margin: '0 3px' }} />,
+              <Tooltip title="第三方科室不可删除!">
+                <span style={{ cursor: 'not-allowed', color: 'rgb(176 181 189)' }}>删除</span>
+              </Tooltip>,
+            ];
+          }
+        };
         return [
-          <UpDataActBtn key={'act'} record={record} type="EDIT" />,
-          <Popconfirm title="是否确认删除?" key="del" onConfirm={() => delTableData(record)}>
-            <a>删除</a>
-          </Popconfirm>,
+             ...(getEditBtn())
         ];
       },
     },
@@ -229,13 +254,21 @@ export default function DepartmentMana() {
     );
   };
 
+  const handleTableChange = (pagination: any) => {
+    set_tableDataFilterParams({
+      ...tableDataFilterParams,
+      current: pagination.current,
+      pageSize: pagination.pageSize
+    });
+  }
+
   const tableDataSearchHandle = (paramName: string) => {
     set_tableDataFilterParams({
       ...tableDataFilterParams,
       current: 1,
-      [`${paramName}`]: tableDataSearchKeywords,
+      [`${paramName}`]: tableDataSearchKeywords
     });
-  };
+  }
 
   const downloadTemplateFileHandle = async () => {
     await downloadTemplateReq('/centerSys/sysdepartment/export');
@@ -348,7 +381,16 @@ export default function DepartmentMana() {
         </div>
       </div>
       <div style={{ marginTop: 16 }}>
-        <KCTable columns={columns as ProColumns[]} scroll={{ y: `calc(100vh - 255px)` }} reload={reload} rowKey="id" newVer params={tableDataFilterParams} request={(params) => getTableData(params)} />
+        <KCTable
+          columns={columns}
+          scroll={{ y: `calc(100vh - 250px)` }}
+          reload={reload}
+          rowKey="id"
+          newVer
+          params={tableDataFilterParams}
+          request={(params) => getTableData(params)}
+          onChange={handleTableChange}
+        />
       </div>
     </div>
   );

+ 8 - 9
src/pages/platform/setting/embeddedDashboard/index.tsx

@@ -6,22 +6,21 @@ function MyEmbeddedDashboard() {
   // 从 URL 中获取仪表板 ID
   const paths = window.location.pathname.split('/').filter((item) => item);
   const dashboardId = paths[paths.length - 1];
-  console.log('paths', paths);
-  console.log('dashboardId:', dashboardId);
+  const host = window.location.hostname;
 
   // 封装获取 Guest Token 的异步函数
   const getGuestToken = async () => {
     try {
       // 1. 登录,获取 access_token
-      const loginResponse = await fetch('/api/v1/security/login', {
+      const loginResponse = await fetch(`http://${host}:8088/api/v1/security/login`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
         },
         body: JSON.stringify({
-          username: 'admin',
-          password: 'admin',
-          provider: 'db', // 根据你的实际配置,比如 db 或者其他
+          username: 'kcim',
+          password: 'kcim123456',
+          provider: 'db', 
           refresh: true,
         }),
       });
@@ -33,7 +32,7 @@ function MyEmbeddedDashboard() {
       }
 
       // 2. 使用 access_token 调用 /api/v1/security/guest_token/ 获取 Guest Token
-      const guestTokenResponse = await fetch('/api/v1/security/guest_token/', {
+      const guestTokenResponse = await fetch(`http://${host}:8088/api/v1/security/guest_token/`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
@@ -77,7 +76,7 @@ function MyEmbeddedDashboard() {
     if (dashboardId) {
       embedDashboard({
         id: dashboardId,
-        supersetDomain: 'http://localhost:8088', // 你的 Superset 服务地址
+        supersetDomain: `http://${host}:8088`, 
         mountPoint: document.getElementById('dashboard-container') as HTMLElement,
         dashboardUiConfig: {
           hideTitle: true,
@@ -97,7 +96,7 @@ function MyEmbeddedDashboard() {
 
   return (
     <div className="my-embedded-dashboard">
-      <div className="dashboard" id="dashboard-container" style={{ width: '100%', height: '80vh' }} />
+      <div className="dashboard" id="dashboard-container"  />
     </div>
   );
 }

+ 8 - 0
src/pages/platform/setting/embeddedDashboard/style.less

@@ -1,5 +1,13 @@
 .my-embedded-dashboard {
+  width: 100%;
+  height: 100%;
+  border-radius: 4px;
+  overflow: hidden;
   .dashboard {
+    width: 100%;
+    height: 100%;
+    border-radius: 4px;
+    overflow: hidden;
     & > iframe {
       width: 100%;
       height: 100%;

+ 32 - 16
src/pages/platform/setting/hospManage/index.tsx

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2022-01-13 15:22:48
- * @LastEditTime: 2025-02-18 15:09:49
+ * @LastEditTime: 2025-05-22 15:53:18
  * @LastEditors: code4eat awesomedema@gmail.com
  * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  * @FilePath: /KC-MiddlePlatform/src/pages/platform/setting/hospManage/index.tsx
@@ -711,7 +711,12 @@ const DrawerActBtn = ({ record }: { record: any }) => {
 const HospManage: FC<PageProps> = ({ hospManageModel: state, dispatch }) => {
   const { reloadTable } = state;
   const { initialState, setInitialState } = useModel('@@initialState');
-  const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>();
+  const [tableDataFilterParams, set_tableDataFilterParams] = useState<TableRequestParamsType>({
+    current: 1,
+    pageSize: 10,
+    name: '',
+    hospName: ''
+  });
   const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
   const [tabIds, set_tabIds] = useState<Key[]>([]);
   const [open, set_open] = useState(false);
@@ -1005,9 +1010,17 @@ const HospManage: FC<PageProps> = ({ hospManageModel: state, dispatch }) => {
     set_tableDataFilterParams({
       ...tableDataFilterParams,
       current: 1,
-      [`${paramName}`]: tableDataSearchKeywords,
+      [`${paramName}`]: tableDataSearchKeywords
     });
-  };
+  }
+
+  const handleTableChange = (pagination: any) => {
+    set_tableDataFilterParams({
+      ...tableDataFilterParams,
+      current: pagination.current,
+      pageSize: pagination.pageSize
+    });
+  }
 
   const onCheckedHandle = async (data: NavSelecterItemType[]) => {
     dispatch &&
@@ -1026,7 +1039,7 @@ const HospManage: FC<PageProps> = ({ hospManageModel: state, dispatch }) => {
           <div className="filterItem">
             <span className="label">检索:</span>
             <KCInput
-              placeholder={'请输入院名称'}
+              placeholder={'请输入院名称'}
               style={{ width: 160 }}
               search
               allowClear
@@ -1035,7 +1048,8 @@ const HospManage: FC<PageProps> = ({ hospManageModel: state, dispatch }) => {
                 if (e.target.value.length == 0) {
                   set_tableDataFilterParams({
                     ...tableDataFilterParams,
-                    hospName: '',
+                    current: 1,
+                    hospName: ''
                   });
                 }
               }}
@@ -1049,16 +1063,18 @@ const HospManage: FC<PageProps> = ({ hospManageModel: state, dispatch }) => {
           </span>
         </div>
       </div>
-      <KCTable
-        newVer
-        rowKey="id"
-        scroll={{ y: `calc(100vh - 250px)` }}
-        columns={columns}
-        reload={reloadTable}
-        options={false}
-        params={tableDataFilterParams}
-        request={(params) => getHospData(params)}
-      />
+      <div style={{ marginTop: 16 }}>
+        <KCTable
+          columns={columns}
+          scroll={{ y: `calc(100vh - 250px)` }}
+          reload={reloadTable}
+          rowKey="id"
+          newVer
+          params={tableDataFilterParams}
+          request={(params) => getHospData(params)}
+          onChange={handleTableChange}
+        />
+      </div>
     </div>
   );
 };

+ 28 - 17
src/pages/platform/setting/hospParamsMana/index.tsx

@@ -33,7 +33,11 @@ const SearchIcon = createFromIconfontCN({
 });
 
 export default function PubDicTypeMana() {
-  const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>();
+  const [tableDataFilterParams, set_tableDataFilterParams] = useState<TableRequestParamsType>({
+    current: 1,
+    pageSize: 10,
+    name: ''
+  });
   const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
   const [reload, set_reload] = useState(false);
   const [currentEdit, set_currentEdit] = useState<any>(undefined);
@@ -258,9 +262,17 @@ export default function PubDicTypeMana() {
     set_tableDataFilterParams({
       ...tableDataFilterParams,
       current: 1,
-      [`${paramName}`]: tableDataSearchKeywords,
+      [`${paramName}`]: tableDataSearchKeywords
     });
-  };
+  }
+
+  const handleTableChange = (pagination: any) => {
+    set_tableDataFilterParams({
+      ...tableDataFilterParams,
+      current: pagination.current,
+      pageSize: pagination.pageSize
+    });
+  }
 
   const getTreeReqFunc = async (name?: string) => {
     const resp = await getTreeData();
@@ -434,7 +446,7 @@ export default function PubDicTypeMana() {
             <div className="filterItem">
               <span className="label">检索:</span>
               <KCInput
-                placeholder={'请输入名称'}
+                placeholder={'请输入参数名称'}
                 style={{ width: 160 }}
                 search
                 allowClear
@@ -444,28 +456,27 @@ export default function PubDicTypeMana() {
                     set_tableDataFilterParams({
                       ...tableDataFilterParams,
                       current: 1,
-                      targetName: '',
+                      name: ''
                     });
                   }
                 }}
-                onSearch={() => tableDataSearchHandle('targetName')}
+                onSearch={() => tableDataSearchHandle('name')}
               />
             </div>
           </div>
           <div className="btnGroup">{currentSelectedTreeNode?.status == 1 && <UpDataActBtn record type="ADD" />}</div>
         </div>
         <div style={{ marginTop: 16 }}>
-          {currentSelectedTreeNode && (
-            <KCTable
-              scroll={{ y: `calc(100vh - 315px)` }}
-              columns={columns as ProColumns[]}
-              reload={reload}
-              rowKey="id"
-              newVer
-              params={tableDataFilterParams}
-              request={(params) => getTableData(params)}
-            />
-          )}
+          <KCTable
+            columns={columns}
+            scroll={{ y: `calc(100vh - 250px)` }}
+            reload={reload}
+            rowKey="id"
+            newVer
+            params={tableDataFilterParams}
+            request={(params) => getTableData(params)}
+            onChange={handleTableChange}
+          />
         </div>
       </div>
     </div>

+ 28 - 40
src/pages/platform/setting/indicatoLagacy/index.tsx

@@ -33,7 +33,16 @@ const IndicatorMana = () => {
   const [tableData, set_tableData] = useState<any[]>([]);
   const [cateId, set_cateId] = useState<string>();
 
-  const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>();
+  const [tableDataFilterParams, set_tableDataFilterParams] = useState<{
+    current: number;
+    pageSize: number;
+    name?: string;
+    indicatorType?: string;
+    outsideIndexes?: string[];
+  }>({
+    current: 1,
+    pageSize: 10
+  });
   const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
   const [currentSelectedLeft, set_currentSelectedLeft] = useState<any>(undefined);
 
@@ -184,11 +193,11 @@ const IndicatorMana = () => {
   };
 
   const getData = async (params: any) => {
-    const { current = 1, name, indicatorType, outsideIndexes } = params;
+    const { current = 1, pageSize = 10, name, indicatorType, outsideIndexes } = params;
     if (cateId) {
       const resp = await getIndicatorManaList_old({
-        current: 1,
-        pageSize: 0,
+        current,
+        pageSize,
         menuCode: cateId as string,
         indicatorName: name,
         indicatorType: indicatorType,
@@ -346,13 +355,21 @@ const IndicatorMana = () => {
     currentSelected && set_cateId(currentSelected.code);
   };
 
+  const handleTableChange = (pagination: any) => {
+    set_tableDataFilterParams({
+      ...tableDataFilterParams,
+      current: pagination.current,
+      pageSize: pagination.pageSize
+    });
+  }
+
   const tableDataSearchHandle = (paramName: string) => {
     set_tableDataFilterParams({
       ...tableDataFilterParams,
       current: 1,
-      [`${paramName}`]: tableDataSearchKeywords,
+      [`${paramName}`]: tableDataSearchKeywords
     });
-  };
+  }
 
   useEffect(() => {
     if (tableData) {
@@ -421,43 +438,14 @@ const IndicatorMana = () => {
           </div>
           {cateId && (
             <KCTable
-              newVer
-              rowKey="menuId"
               columns={columns}
-              params={tableDataFilterParams}
-              actionRef={indicatorTableRef}
-              scroll={{ y: `calc(100vh - 228px)` }}
-              expandable={{
-                expandedRowKeys: defaultExpandedRowKeys,
-                onExpand: (expanded, record) => {
-                  // console.log({expanded, record});
-                  let _expandedKeys = [...defaultExpandedRowKeys];
-                  if (expanded) {
-                    _expandedKeys.push(record.menuId);
-                  } else {
-                    const index = _expandedKeys.findIndex((t) => t == `${record.menuId}`);
-                    if (index != -1) {
-                      _expandedKeys.splice(index, 1);
-                    }
-                  }
-                  set_defaultExpandedRowKeys([..._expandedKeys]);
-                },
-              }}
+              scroll={{ y: `calc(100vh - 250px)` }}
               reload={reloadTable}
-              pagination={false}
-              // toolBarRender={() => [
-              //   // <Button key="1" onClick={addHandle}>
-              //   //   上传
-              //   // </Button>,
-              //   // <Button key="2" onClick={addHandle}>
-              //   //   下载
-              //   // </Button>,
-              //   <Button key="3" type="primary" onClick={addHandle}>
-              //     新增
-              //   </Button>,
-              // ]}
-              options={false}
+              rowKey="id"
+              newVer
+              params={tableDataFilterParams}
               request={(params) => getData(params)}
+              onChange={handleTableChange}
             />
           )}
         </div>

+ 50 - 0
src/pages/platform/setting/indicatorMana/frameContainer/index.tsx

@@ -0,0 +1,50 @@
+/*
+ * @Author: code4eat awesomedema@gmail.com
+ * @Date: 2025-04-07 17:34:56
+ * @LastEditors: code4eat awesomedema@gmail.com
+ * @LastEditTime: 2025-04-08 14:33:52
+ * @FilePath: /KC-MiddlePlatform/src/pages/platform/setting/indicatorMana/frameContainer/index.tsx
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ */
+import React, { useState } from 'react';
+
+const FrameComponent = ({ link }:{link:string}) => {
+  const [loading, setLoading] = useState(true);
+
+  const handleLoad = () => {
+    setLoading(false);
+  };
+
+  return (
+    <div style={{ position: 'relative', width: '100%', height: '100%',borderRadius:4,overflow:'hidden' }}>
+      {loading && (
+        <div
+          style={{
+            position: 'absolute',
+            top: 0,
+            left: 0,
+            width: '100%',
+            height: '100%',
+            display: 'flex',
+            alignItems: 'center',
+            justifyContent: 'center',
+            background: '#fff',
+            zIndex: 1,
+          }}
+        >
+          加载中...
+        </div>
+      )}
+      <iframe
+        src={link}
+        title="Dynamic Frame"
+        width="100%"
+        height="100%"
+        style={{ border: 'none' }}
+        onLoad={handleLoad}
+      />
+    </div>
+  );
+};
+
+export default FrameComponent;

+ 128 - 32
src/pages/platform/setting/indicatorMana/index.tsx

@@ -2,19 +2,21 @@
  * @Author: code4eat awesomedema@gmail.com
  * @Date: 2022-07-12 11:14:21
  * @LastEditors: code4eat awesomedema@gmail.com
- * @LastEditTime: 2025-02-28 14:52:26
+ * @LastEditTime: 2025-04-25 17:40:50
  * @FilePath: /KC-MiddlePlatform/src/pages/platform/setting/indicatorMana/index.tsx
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */
 
 import { ActionType, ProColumns } from '@ant-design/pro-table';
 import React, { useEffect, useRef, useState } from 'react';
-import { Dropdown, Empty, Input, Popconfirm, Tabs } from 'antd';
+import { Dropdown, Empty, Input, Popconfirm, Tabs, Modal, Form, message } from 'antd';
+import { ModalForm, ProFormInstance, ProFormText, ProFormTextArea } from '@ant-design/pro-form';
 
 import DrawerForm from './DrawerForm/drawer';
 import { createFromIconfontCN, DownOutlined } from '@ant-design/icons';
 import {
   addIndicatorManaList,
+  applyOnlineReportMap,
   delIndicatorManaList,
   editIndicatorManaList,
   getIndicatorCateList,
@@ -32,6 +34,7 @@ import KCTable from '@/components/kcTable';
 import { KCInput } from '@/components/KCInput';
 import generateTableData from '../dataFilling/fillingMana/generateTableData';
 import { DATAFILL_PERIODTYPE } from '../dataFilling/fillingMana';
+import FrameComponent from './frameContainer';
 
 const defaultYear = new Date().getFullYear();
 let table_columns: any[] = [];
@@ -51,7 +54,11 @@ const IndicatorMana = () => {
 
   const [cateId, set_cateId] = useState<string>();
   const [tableColumns, set_tableColumns] = useState<ProColumns[]>([]);
-  const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>();
+  const [tableDataFilterParams, set_tableDataFilterParams] = useState<TableRequestParamsType>({
+    current: 1,
+    pageSize: 10,
+    name: '',
+  });
   const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
   const [currentSelectedLeft, set_currentSelectedLeft] = useState<any>(undefined);
   const [actionType, set_actionType] = useState<'NORMAL' | 'DETAIL' | undefined>('NORMAL');
@@ -67,6 +74,9 @@ const IndicatorMana = () => {
   const [currentTab, set_currentTab] = useState('1');
   const { indicatorType: indicatorUrlType }: { id: string; indicatorType: string } = history.location.query;
 
+  const [reportModalVisible, setReportModalVisible] = useState(false);
+  const reportForm = useRef<ProFormInstance>();
+
   const columns: ProColumns<any>[] = [
     {
       title: '指标名称',
@@ -228,7 +238,7 @@ const IndicatorMana = () => {
   };
 
   const getData = async (params: any) => {
-    const { current = 1, name, indicatorType, outsideIndexes } = params;
+    const { current = 1, pageSize = 10, name, indicatorType, outsideIndexes } = params;
     if (cateId) {
       const resp = await getIndicatorManaList({
         menuCode: cateId as string,
@@ -236,40 +246,21 @@ const IndicatorMana = () => {
         indicatorName: name,
         indicatorType: indicatorType,
         outsideIndexes: outsideIndexes && outsideIndexes.length > 0 ? outsideIndexes[outsideIndexes.length - 1] : '',
+        current,
+        pageSize
       });
 
       if (resp && resp[0]) {
-        // const getName: any = (obj: any) => {
-
-        //   let isArr = Array.isArray(obj);
-        //   let reslut = [];
-        //   if (isArr) {
-        //     obj.forEach((item: any) => {
-        //       reslut.push(...getName(item));
-        //     });
-        //   } else {
-        //     reslut.push(obj.menuId)
-
-        //     if (obj.children) {
-        //       reslut.push(...getName(obj.children));
-        //     }
-        //   }
-        //   return reslut;
-
-        // }
-
-        // set_defaultExpandedRowKeys(getName(resp[0].children));
-
         return {
           data: resp[0].children as unknown as [],
           success: true,
-          total: 1,
+          total: resp[0].total || resp[0].children?.length || 0
         };
       } else {
         return {
           data: [],
           success: true,
-          total: 0,
+          total: 0
         };
       }
     }
@@ -277,7 +268,7 @@ const IndicatorMana = () => {
     return {
       data: [],
       success: false,
-      total: 0,
+      total: 0
     };
   };
 
@@ -397,9 +388,17 @@ const IndicatorMana = () => {
     set_tableDataFilterParams({
       ...tableDataFilterParams,
       current: 1,
-      [`${paramName}`]: tableDataSearchKeywords,
+      [`${paramName}`]: tableDataSearchKeywords
     });
-  };
+  }
+
+  const handleTableChange = (pagination: any) => {
+    set_tableDataFilterParams({
+      ...tableDataFilterParams,
+      current: pagination.current,
+      pageSize: pagination.pageSize
+    });
+  }
 
   const detailTableDataSearchHandle = (paramName: string) => {
     const result = total_tableDataSource.filter((a: any) => a[`${paramName}`].indexOf(tableDataSearchKeywords) != -1);
@@ -479,10 +478,99 @@ const IndicatorMana = () => {
     getIndicatorCateTree();
   }, []);
 
+  // // 当弹窗可见性变化时,处理表单回显
+  // useEffect(() => {
+  //   console.log('Modal visibility changed:', reportModalVisible); // 日志 1: 检查可见性变化
+  //   if (reportModalVisible && currentEditRow) {
+  //     console.log('Attempting to set form values. currentEditRow:', currentEditRow); // 日志 2: 检查 currentEditRow 内容
+  //     const valuesToSet = {
+  //       reportId: currentEditRow.onlineReportCode,
+  //       remark: currentEditRow.onlineRemark
+  //     };
+  //     console.log('Values to set:', valuesToSet); // 日志 3: 检查要设置的值
+  //     try {
+  //       reportForm?.current?.setFieldsValue(valuesToSet);
+  //       console.log('setFieldsValue called successfully.'); // 日志 4: 确认调用成功
+  //     } catch (e) {
+  //       console.error('Error calling setFieldsValue:', e); // 日志 5: 检查调用是否出错
+  //     }
+  //   } else if (reportModalVisible) {
+  //       console.log('Modal is visible, but currentEditRow is missing or empty:', currentEditRow); // 日志 6: 检查 currentEditRow 是否为空
+  //   }
+  //   // ModalForm 的 destroyOnClose 会处理关闭时的重置
+  // }, [reportModalVisible, currentEditRow, reportForm]); // 依赖项
+
+  // 打开报告管理弹窗
+  const openReportModal = () => {
+    setReportModalVisible(true);
+  };
+
+  // 处理报告表单提交
+  const handleReportSubmit = async (values: any) => {
+    try {
+      const resp = await applyOnlineReportMap({
+        code: currentEditRow.code,
+        onlineReportCode: values.reportId,
+        onlineRemark: values.remark
+      });
+      if (resp) {
+        message.success('提交成功');
+        set_currentEditRow((prevRow: any) => ({
+          ...prevRow,
+          onlineRemark:values.remark,
+          onlineReportCode: values.reportId
+        }));
+        return true; // 提交成功后关闭弹窗
+      }
+    } catch (error) {
+      console.error('表单验证或提交失败:', error);
+      message.error('提交失败'); // 添加错误提示
+      return false; // 提交失败,保持弹窗打开
+    }
+    // 如果 API 调用失败 (resp 为 false),显式返回 false
+    message.error('提交失败');
+    return false;
+  };
+
   return (
     <div className="IndicatorMana">
       <DrawerForm actType={drawerActype} visible={drawerVisible} onVisibleChange={onVisibleChangeHandle} onFinish={(data) => drawerFormCommitHanndle(data)} record={currentEditRowData} />
 
+      {/* 报告管理弹窗 */}
+      <ModalForm
+        title="报告管理"
+        visible={reportModalVisible}
+        onVisibleChange={setReportModalVisible}
+        onFinish={handleReportSubmit}
+        width={500}
+        initialValues={currentEditRow&&{reportId: currentEditRow.onlineReportCode,
+          remark: currentEditRow.onlineRemark}}
+        formRef={reportForm}
+        modalProps={{ 
+          destroyOnClose: true,
+          bodyStyle: {
+            maxHeight: '72vh',
+            overflowY: 'auto',
+            overflowX: 'hidden'
+          }
+        }}
+      >
+        <ProFormText
+          name="reportId"
+          label="关联报告ID"
+          placeholder="请输入"
+          rules={[{ required: true, message: '请输入关联报告ID' }]}
+        />
+        <ProFormTextArea
+          name="remark"
+          label="备注说明"
+          placeholder="请输入"
+          fieldProps={{
+            rows: 4,
+          }}
+        />
+      </ModalForm>
+
       {actionType == 'DETAIL' && (
         <div className="FillingMana-detail">
           <div className="FillingMana-detail-header">
@@ -508,8 +596,15 @@ const IndicatorMana = () => {
 
           {currentBigTab === '1' && (
             <div className="currentBigTab1content">
-              <img src={require('../../../../../public/images/empty_bigger.png')} alt="" />
-              <div className="detail-noAuthRecord-title">暂无内容</div>
+              {/* <img src={require('../../../../../public/images/empty_bigger.png')} alt="" />
+              <div className="detail-noAuthRecord-title">暂无内容</div> */}
+              <div className='currentBigTab1content_header'>
+                <span>{currentEditRow?.onlineRemark}</span>
+                <div className='btn' onClick={openReportModal}>
+                  <IconFont type='iconpeizhi' /> 报告管理
+                </div>
+              </div>
+              {(currentEditRow&&currentEditRow.onlineReportCode)&&<FrameComponent link={`/platform/setting/embeddedDashboard/${currentEditRow.onlineReportCode}?noTopbar=true&noMenu=true`} />}
             </div>
           )}
 
@@ -636,6 +731,7 @@ const IndicatorMana = () => {
                 pagination={false}
                 options={false}
                 request={(params) => getData(params)}
+                onChange={handleTableChange}
               />
             )}
           </div>

+ 54 - 37
src/pages/platform/setting/indicatorMana/style.less

@@ -89,51 +89,68 @@
     }
 
     .currentBigTab1content {
-      display: flex;
-      width: 100%;
+
       height: calc(100vh - 210px);
-      flex-direction: column;
-      justify-content: center;
-      align-items: center;
       padding: 16px;
       background: #ffffff;
       border-radius: 4px;
 
-      & > img {
-        width: 160px;
-        height: 140px;
-      }
+      // & > img {
+      //   width: 160px;
+      //   height: 140px;
+      // }
 
-      .detail-noAuthRecord-title {
-        font-weight: 500;
-        font-size: 24px;
-        height: 24px;
-        color: #17181a;
-        line-height: 24px;
-        margin-top: 24px;
-        margin-bottom: 16px;
-      }
+      // .detail-noAuthRecord-title {
+      //   font-weight: 500;
+      //   font-size: 24px;
+      //   height: 24px;
+      //   color: #17181a;
+      //   line-height: 24px;
+      //   margin-top: 24px;
+      //   margin-bottom: 16px;
+      // }
 
-      .detail-noAuthRecord-title-sub {
-        font-weight: 400;
-        font-size: 14px;
-        height: 14px;
-        color: #7a8599;
-        line-height: 14px;
-      }
+      // .detail-noAuthRecord-title-sub {
+      //   font-weight: 400;
+      //   font-size: 14px;
+      //   height: 14px;
+      //   color: #7a8599;
+      //   line-height: 14px;
+      // }
+
+      // .backBtn {
+      //   width: 56px;
+      //   height: 24px;
+      //   text-align: center;
+      //   margin-top: 24px;
+      //   background: #3377ff;
+      //   border-radius: 4px;
+      //   font-weight: 400;
+      //   font-size: 14px;
+      //   color: #ffffff;
+      //   line-height: 24px;
+      //   cursor: pointer;
+      // }
 
-      .backBtn {
-        width: 56px;
-        height: 24px;
-        text-align: center;
-        margin-top: 24px;
-        background: #3377ff;
-        border-radius: 4px;
-        font-weight: 400;
-        font-size: 14px;
-        color: #ffffff;
-        line-height: 24px;
-        cursor: pointer;
+      .currentBigTab1content_header {
+        width: 100%;
+        display: flex;
+        flex-direction: row;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 16px;
+        &>span {
+            font-size: 16px;
+        }
+        .btn {
+          cursor: pointer;
+          padding: 2px 6px;
+          font-size: 16px;
+          color: #3376fe;
+          border-radius: 4px;
+          text-align: center;
+          border: 1px solid #3376fe;
+        }
       }
     }
 

+ 47 - 8
src/pages/platform/setting/kcClassification/index.tsx

@@ -22,7 +22,14 @@ import './style.less';
 import { findParents, renameChildListToChildren, searchTreeAndKeepStructure } from '@/utils';
 
 export default function KcClassification() {
-  const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>();
+  const [tableDataFilterParams, set_tableDataFilterParams] = useState<{
+    current: number;
+    pageSize: number;
+    name?: string;
+  }>({
+    current: 1,
+    pageSize: 10
+  });
   const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
   const [reload, set_reload] = useState(false);
   const [currentEdit, set_currentEdit] = useState<any>(undefined);
@@ -63,16 +70,31 @@ export default function KcClassification() {
     },
   ];
 
-  const getTableData = async () => {
+  const getTableData = async (params?: any) => {
     const resp = await getData();
     set_reload(false);
     if (resp) {
       const data = renameChildListToChildren(resp, 'childList');
       set_tableData(data);
       set_dataSource(data);
+      
+      // 处理分页
+      const { current = 1, pageSize = 10, name } = params || {};
+      const start = (current - 1) * pageSize;
+      const end = start + pageSize;
+      const filteredData = name ? searchTreeAndKeepStructure(data, 'name', name) : data;
+      
+      return {
+        data: filteredData.slice(start, end),
+        success: true,
+        total: filteredData.length
+      };
     }
-    set_reload(false);
-    return [];
+    return {
+      data: [],
+      success: false,
+      total: 0
+    };
   };
 
   const delTableData = async (record: any) => {
@@ -156,13 +178,21 @@ export default function KcClassification() {
     );
   };
 
+  const handleTableChange = (pagination: any) => {
+    set_tableDataFilterParams({
+      ...tableDataFilterParams,
+      current: pagination.current,
+      pageSize: pagination.pageSize
+    });
+  }
+
   const tableDataSearchHandle = (paramName: string) => {
     set_tableDataFilterParams({
       ...tableDataFilterParams,
-      current:1,
-      [`${paramName}`]: tableDataSearchKeywords,
+      current: 1,
+      [`${paramName}`]: tableDataSearchKeywords
     });
-  };
+  }
 
   useEffect(() => {
     if (reload) {
@@ -214,7 +244,16 @@ export default function KcClassification() {
         </div>
       </div>
       <div style={{ marginTop: 16 }}>
-        <KCTable scroll={{ y: `calc(100vh - 195px)` }} pagination={false} columns={columns as ProColumns[]} rowKey="id" newVer dataSource={tableData} />
+        <KCTable
+          columns={columns}
+          scroll={{ y: `calc(100vh - 250px)` }}
+          reload={reload}
+          rowKey="id"
+          newVer
+          params={tableDataFilterParams}
+          request={(params) => getTableData(params)}
+          onChange={handleTableChange}
+        />
       </div>
     </div>
   );

+ 41 - 12
src/pages/platform/setting/notificationTemplate/index.tsx

@@ -26,7 +26,14 @@ import './style.less';
 
 export default function PubDicTypeMana() {
 
-    const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>();
+    const [tableDataFilterParams, set_tableDataFilterParams] = useState<{
+        current: number;
+        pageSize: number;
+        name?: string;
+    }>({
+        current: 1,
+        pageSize: 10
+    });
     const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
     const [reload, set_reload] = useState(false);
     const [currentEdit, set_currentEdit] = useState<any>(undefined);
@@ -102,18 +109,25 @@ export default function PubDicTypeMana() {
 
 
     const getTableData = async (params: any) => {
-        const resp = await getData(params);
+        const { current = 1, pageSize = 10, name } = params;
+        const resp = await getData({
+            current,
+            pageSize,
+            name
+        });
         set_reload(false);
         if (resp) {
             return {
                 data: resp.list,
                 success: true,
-                total: resp.totalCount,
-                pageSize: resp.pageSize,
-                totalPage: resp.totalPage,
-            }
+                total: resp.totalCount
+            };
         }
-        return []
+        return {
+            data: [],
+            success: false,
+            total: 0
+        };
     }
 
     const delTableData = async (record: any) => {
@@ -304,14 +318,20 @@ export default function PubDicTypeMana() {
         )
     }
 
-    const tableDataSearchHandle = (paramName: string) => {
-
+    const handleTableChange = (pagination: any) => {
+        set_tableDataFilterParams({
+            ...tableDataFilterParams,
+            current: pagination.current,
+            pageSize: pagination.pageSize
+        });
+    }
 
+    const tableDataSearchHandle = (paramName: string) => {
         set_tableDataFilterParams({
             ...tableDataFilterParams,
-            current:1,
+            current: 1,
             [`${paramName}`]: tableDataSearchKeywords
-        })
+        });
     }
 
 
@@ -405,7 +425,16 @@ export default function PubDicTypeMana() {
                 </div>
             </div>
             <div style={{ marginTop: 16 }}>
-                <KCTable  scroll={{y:`calc(100vh - 250px)`}} columns={columns as ProColumns[]} reload={reload} rowKey='id' newVer params={tableDataFilterParams} request={(params) => getTableData(params)} />
+                <KCTable
+                    columns={columns}
+                    scroll={{ y: `calc(100vh - 250px)` }}
+                    reload={reload}
+                    rowKey="id"
+                    newVer
+                    params={tableDataFilterParams}
+                    request={(params) => getTableData(params)}
+                    onChange={handleTableChange}
+                />
             </div>
         </div>
     )

+ 28 - 8
src/pages/platform/setting/paramsMana/index.tsx

@@ -33,7 +33,13 @@ const SearchIcon = createFromIconfontCN({
 
 export default function ParamsMana() {
 
-    const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>();
+    const [tableDataFilterParams, set_tableDataFilterParams] = useState<TableRequestParamsType>({
+        current: 1,
+        pageSize: 10,
+        paramName: '',
+        paramCode: '',
+        paramType: '',
+    });
     const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
     const [reload, set_reload] = useState(false);
     const [currentEdit, set_currentEdit] = useState<any>(undefined);
@@ -196,15 +202,30 @@ export default function ParamsMana() {
 
 
     const tableDataSearchHandle = (paramName: string) => {
-
-
         set_tableDataFilterParams({
             ...tableDataFilterParams,
-            current:1,
+            current: 1,
             [`${paramName}`]: tableDataSearchKeywords
-        })
+        });
     }
 
+    const handleTableChange = (pagination: any) => {
+        set_tableDataFilterParams({
+            ...tableDataFilterParams,
+            current: pagination.current,
+            pageSize: pagination.pageSize
+        });
+    }
+
+    const tableDataResetHandle = () => {
+        set_tableDataFilterParams({
+            current: 1,
+            pageSize: 10,
+            paramName: '',
+            paramCode: '',
+            paramType: '',
+        });
+    }
 
     const getTreeReqFunc = async () => {
         const resp = await getSysLists();
@@ -392,13 +413,12 @@ export default function ParamsMana() {
                                     if (e.target.value.length == 0) {
                                         set_tableDataFilterParams({
                                             ...tableDataFilterParams,
-                                            current:1,
+                                            current: 1,
                                             parameterName: ''
                                         });
                                     }
                                 }}
                                 onSearch={() => tableDataSearchHandle('parameterName')}
-
                             />
                         </div>
                     </div>
@@ -407,7 +427,7 @@ export default function ParamsMana() {
                     </div>
                 </div>
                 <div style={{ marginTop: 16 }}>
-                    {currentSelectedTreeNode && <KCTable columns={columns} scroll={{ y: `calc(100vh - 250px)` }} reload={reload} rowKey='id' newVer params={tableDataFilterParams} request={(params) => getTableData(params)} />}
+                    {currentSelectedTreeNode && <KCTable columns={columns} scroll={{ y: `calc(100vh - 250px)` }} reload={reload} rowKey='id' newVer params={tableDataFilterParams} request={(params) => getTableData(params)} onChange={handleTableChange} />}
                 </div>
             </div>
 

+ 21 - 5
src/pages/platform/setting/pubDicMana/index.tsx

@@ -39,7 +39,14 @@ if (currentSelectedTab) {
 }
 
 const PubDicMana = () => {
-  const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>();
+  const [tableDataFilterParams, set_tableDataFilterParams] = useState<{
+    current: number;
+    pageSize: number;
+    name?: string;
+  }>({
+    current: 1,
+    pageSize: 10
+  });
   const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
   const [reload, set_reload] = useState(false);
 
@@ -212,13 +219,21 @@ const PubDicMana = () => {
     );
   };
 
+  const handleTableChange = (pagination: any) => {
+    set_tableDataFilterParams({
+      ...tableDataFilterParams,
+      current: pagination.current,
+      pageSize: pagination.pageSize
+    });
+  }
+
   const tableDataSearchHandle = (paramName: string) => {
     set_tableDataFilterParams({
       ...tableDataFilterParams,
       current: 1,
-      [`${paramName}`]: tableDataSearchKeywords,
+      [`${paramName}`]: tableDataSearchKeywords
     });
-  };
+  }
 
   const getTreeReqFunc = async (type: number) => {
     const resp = await getLeftData(type);
@@ -461,13 +476,14 @@ const PubDicMana = () => {
         <div style={{ marginTop: 16 }}>
           {currentSelectedTreeNode && (
             <KCTable
+              columns={columns}
               scroll={{ y: `calc(100vh - 250px)` }}
-              columns={columns as ProColumns[]}
               reload={reload}
-              rowKey={pageType == 1 ? 'dictDataId' : 'id'}
+              rowKey="id"
               newVer
               params={tableDataFilterParams}
               request={(params) => getTableData(params)}
+              onChange={handleTableChange}
             />
           )}
         </div>

+ 22 - 6
src/pages/platform/setting/pubDicTypeMana/index.tsx

@@ -31,7 +31,14 @@ const SearchIcon = createFromIconfontCN({
 });
 
 export default function PubDicTypeMana() {
-  const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>();
+  const [tableDataFilterParams, set_tableDataFilterParams] = useState<{
+    current: number;
+    pageSize: number;
+    name?: string;
+  }>({
+    current: 1,
+    pageSize: 10
+  });
   const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
   const [reload, set_reload] = useState(false);
   const [currentEdit, set_currentEdit] = useState<any>(undefined);
@@ -190,13 +197,21 @@ export default function PubDicTypeMana() {
     );
   };
 
+  const handleTableChange = (pagination: any) => {
+    set_tableDataFilterParams({
+      ...tableDataFilterParams,
+      current: pagination.current,
+      pageSize: pagination.pageSize
+    });
+  }
+
   const tableDataSearchHandle = (paramName: string) => {
     set_tableDataFilterParams({
       ...tableDataFilterParams,
       current: 1,
-      [`${paramName}`]: tableDataSearchKeywords,
+      [`${paramName}`]: tableDataSearchKeywords
     });
-  };
+  }
 
   const getTreeReqFunc = async (name?: string) => {
     const resp = await getTreeData();
@@ -400,13 +415,14 @@ export default function PubDicTypeMana() {
         <div style={{ marginTop: 16 }}>
           {currentSelectedTreeNode && (
             <KCTable
-              scroll={{ y: `calc(100vh - 255px)` }}
-              columns={columns as ProColumns[]}
+              columns={columns}
+              scroll={{ y: `calc(100vh - 250px)` }}
               reload={reload}
-              rowKey="dictId"
+              rowKey="id"
               newVer
               params={tableDataFilterParams}
               request={(params) => getTableData(params)}
+              onChange={handleTableChange}
             />
           )}
         </div>

+ 133 - 54
src/pages/platform/setting/roleManage/index.tsx

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2022-01-13 15:22:48
- * @LastEditTime: 2025-02-21 17:50:31
+ * @LastEditTime: 2025-05-21 17:50:49
  * @LastEditors: code4eat awesomedema@gmail.com
  * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  * @FilePath: /KC-MiddlePlatform/src/pages/platform/setting/hospManage/index.tsx
@@ -131,14 +131,17 @@ const DrawerActBtn = ({ record }: { record: any }) => {
       key: 'deptName',
       render: (_: any, record: any) => {
         if (record.functionList) {
-          const options = record.functionList.map((item: any, index: number) => ({ label: item.name, value: item.code }));
+          const options = record.functionList.map((item: any, index: number) => ({ 
+            label: item.name, 
+            value: item.code 
+          }));
 
           const needItem = checkBoxCodes.filter((a) => a.menuId == record.menuId);
-
           const codes = needItem && needItem.length > 0 ? needItem[0].function.map((a: any) => a.code) : [];
 
           const onCheckGroupChange = (checkedValue: CheckboxValueType[]) => {
             if (checkedValue.length > 0) {
+              // 1. 处理功能权限数据
               const _temp = [...checkBoxCodes];
               const index = checkBoxCodes.findIndex((item) => item.menuId == record.menuId);
 
@@ -146,7 +149,6 @@ const DrawerActBtn = ({ record }: { record: any }) => {
               const transfered = needed.map((item: any) => ({ name: item.label, code: item.value }));
 
               if (index >= 0) {
-                //先去除旧的的对象
                 _temp.splice(index, 1);
               }
 
@@ -155,19 +157,30 @@ const DrawerActBtn = ({ record }: { record: any }) => {
                 function: transfered,
               });
 
-              set_checkedMenuParentsIds([...checkedMenuParentsIds, record.menuId]);
+              // 2. 获取并添加父级菜单ID
+              const parents = findParents(hospAllMenuTree, record.menuId);
+              const parentsIds = parents ? parents.map((a) => a.menuId) : [];
 
+              // 3. 更新所有相关状态
+              set_checkedMenuParentsIds([...checkedMenuParentsIds, record.menuId, ...parentsIds]);
               set_checkedTableMenuIds([...checkedTableMenuIds, record.menuId]);
-
               set_checkBoxCodes([..._temp]);
             } else {
-              //取消选择
+              // 取消选择时的处理
               const _temp = checkBoxCodes;
               const index = checkBoxCodes.findIndex((item) => item.menuId == record.menuId);
-
-              _temp.splice(index, 1);
-
-              set_checkBoxCodes([..._temp]);
+              
+              if (index >= 0) {
+                _temp.splice(index, 1);
+                set_checkBoxCodes([..._temp]);
+                
+                // 检查是否还有其他功能被选中
+                const hasOtherFunctions = _temp.some(item => item.menuId === record.menuId);
+                if (!hasOtherFunctions) {
+                  // 如果没有其他功能被选中,从选中列表中移除
+                  set_checkedTableMenuIds(checkedTableMenuIds.filter(id => id !== record.menuId));
+                }
+              }
             }
           };
 
@@ -355,39 +368,90 @@ const DrawerActBtn = ({ record }: { record: any }) => {
   };
 
   const saveResult = async () => {
-    let old = [...oldSelectedMenuIds];
-    //const needCancelMenus = needCancelCheckedMenus.map(a=>({hospId: record.id ,systemId: currentSelectedTreeNode.code,function:[],menuId:a}));
-    const result = checkBoxCodes.map((item: any) => {
-      old.splice(
-        old.findIndex((a) => a == item.menuId),
-        1,
-      );
-      return { ...item, hospId: record.hospId, roleId: record.roleId, systemId: currentSelectedTreeNode.code };
-    });
+    try {
+      // 1. 收集所有需要保存的菜单ID
+      const allMenuIds = new Set<string>();
 
-    const needCancelMenus = old.map((a) => ({
-      hospId: record.hospId,
-      systemId: currentSelectedTreeNode.code,
-      menuId: a,
-      roleId: record.roleId,
-      function: [],
-    }));
+      // 2. 添加当前选中的菜单ID(类型转换为string)
+      checkedMenuParentsIds.forEach(id => allMenuIds.add(String(id)));
 
-    const data = {
-      hospId: record.hospId,
-      roleId: record.roleId,
-      systemId: currentSelectedTreeNode.code,
-      menuIds: ifTixiMenu ? (ifTixiMenuAuth ? [currentSelectedTreeNode.code] : []) : [...new Set(checkedMenuParentsIds), currentSelectedTreeNode.code],
-      functionList: [...result, ...needCancelMenus],
-    };
+      // 3. 添加系统ID
+      if (currentSelectedTreeNode?.code) {
+        allMenuIds.add(String(currentSelectedTreeNode.code));
+      }
 
-    const resp = await saveRoleRelaApiPerm(data);
-    if (resp) {
-      set_drawerTablereload(true);
-      set_checkBoxCodes([]);
-      set_checkedTableMenuIds([]);
-      setInitCheckData();
+      // 4. 确保父级ID存在
+      const finalMenuIds = Array.from(allMenuIds).filter(id => {
+        const parents = findParents(hospAllMenuTree, id);
+        if (parents) {
+          return parents.every(parent => allMenuIds.has(String(parent.menuId)));
+        }
+        return true;
+      });
+
+      // 5. 生成 result 和 needCancelMenus
+      let old = [...oldSelectedMenuIds];
+      const result = checkBoxCodes.map((item: any) => {
+        const index = old.findIndex((a) => a == item.menuId);
+        if (index !== -1) {
+          old.splice(index, 1);
+        }
+        return { ...item, hospId: record.hospId, roleId: record.roleId, systemId: currentSelectedTreeNode.code };
+      });
+
+      const needCancelMenus = old.map((a) => ({
+        hospId: record.hospId,
+        systemId: currentSelectedTreeNode.code,
+        menuId: a,
+        roleId: record.roleId,
+        function: [],
+      }));
+
+      const data = {
+        hospId: record.hospId,
+        roleId: record.roleId,
+        systemId: currentSelectedTreeNode.code,
+        menuIds: ifTixiMenu
+          ? (ifTixiMenuAuth ? [currentSelectedTreeNode.code] : [])
+          : finalMenuIds,
+        functionList: [...result, ...needCancelMenus],
+      };
+
+      // 6. 数据验证
+      if (!validateMenuIds(data.menuIds, hospAllMenuTree)) {
+        console.warn('菜单ID数据不完整,请检查');
+        return;
+      }
+
+      const resp = await saveRoleRelaApiPerm(data);
+      if (resp) {
+        set_drawerTablereload(true);
+        set_checkBoxCodes([]);
+        set_checkedTableMenuIds([]);
+        await setInitCheckData();
+      }
+    } catch (error) {
+      console.error('保存角色权限失败:', error);
+    }
+  };
+
+  // 添加数据验证函数
+  const validateMenuIds = (menuIds: string[], tree: any[]) => {
+    // 检查是否所有父级ID都存在
+    const missingParents = menuIds.filter(id => {
+      const parents = findParents(tree, id);
+      if (parents) {
+        return parents.some(parent => !menuIds.includes(parent.menuId));
+      }
+      return false;
+    });
+
+    if (missingParents.length > 0) {
+      console.warn('发现缺失的父级菜单ID:', missingParents);
+      return false;
     }
+
+    return true;
   };
 
   const onCancel = () => {
@@ -699,8 +763,13 @@ const DrawerActBtn = ({ record }: { record: any }) => {
 const RoleManage: FC<PageProps> = ({ roleManageModel: state, dispatch }) => {
   const { reloadTable } = state;
   const { initialState, setInitialState } = useModel('@@initialState');
-  const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>();
+  const [tableDataFilterParams, set_tableDataFilterParams] = useState<TableRequestParamsType>({
+    current: 1,
+    pageSize: 10,
+    roleName: '',
+  });
   const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
+  const [reload, set_reload] = useState(false);
 
   const columns: ProColumns<TableListItem>[] = [
     {
@@ -886,9 +955,17 @@ const RoleManage: FC<PageProps> = ({ roleManageModel: state, dispatch }) => {
     set_tableDataFilterParams({
       ...tableDataFilterParams,
       current: 1,
-      [`${paramName}`]: tableDataSearchKeywords,
+      [`${paramName}`]: tableDataSearchKeywords
     });
-  };
+  }
+
+  const handleTableChange = (pagination: any) => {
+    set_tableDataFilterParams({
+      ...tableDataFilterParams,
+      current: pagination.current,
+      pageSize: pagination.pageSize
+    });
+  }
 
   const editUserBind = async (record: TableListItem) => {
     //编辑角色与人员的绑定关系
@@ -949,7 +1026,7 @@ const RoleManage: FC<PageProps> = ({ roleManageModel: state, dispatch }) => {
                   set_tableDataFilterParams({
                     ...tableDataFilterParams,
                     current: 1,
-                    roleName: '',
+                    roleName: ''
                   });
                 }
               }}
@@ -964,16 +1041,18 @@ const RoleManage: FC<PageProps> = ({ roleManageModel: state, dispatch }) => {
         </div>
       </div>
 
-      <KCTable
-        rowKey="roleId"
-        columns={columns}
-        options={false}
-        newVer
-        params={tableDataFilterParams}
-        reload={reloadTable}
-        scroll={{ y: `calc(100vh - 255px)` }}
-        request={(params) => getData(params)}
-      />
+      <div style={{ marginTop: 16 }}>
+        <KCTable
+          columns={columns}
+          scroll={{ y: `calc(100vh - 250px)` }}
+          reload={reload}
+          rowKey="id"
+          newVer
+          params={tableDataFilterParams}
+          request={(params) => getData(params)}
+          onChange={handleTableChange}
+        />
+      </div>
     </div>
   );
 };

+ 1052 - 0
src/pages/platform/setting/serviceEvaluate/index.tsx

@@ -0,0 +1,1052 @@
+/*
+ * @Author: code4eat awesomedema@gmail.com
+ * @Date: 2025-09-30 00:00:00
+ * @LastEditors: code4eat awesomedema@gmail.com
+ * @LastEditTime: 2025-10-17 11:57:04
+ * @FilePath: /KC-MiddlePlatform/src/pages/platform/setting/serviceEvaluate/index.tsx
+ * @Description: 服务评价管理 - 左树右表 + 可展开选项子表
+ */
+
+import React, { useEffect, useMemo, useState } from 'react';
+import './style.less';
+
+import { Button, Input, InputNumber, message, Modal, Popconfirm, Tooltip, Transfer, Tree } from 'antd';
+import { ProColumns } from '@ant-design/pro-table';
+import { ModalForm, ProFormDigit, ProFormText } from '@ant-design/pro-form';
+import KCTable from '@/components/kcTable';
+import DirectoryTree from 'antd/es/tree/DirectoryTree';
+import { DataNode } from 'antd/es/tree';
+import type { TransferDirection } from 'antd/es/transfer';
+import { createFromIconfontCN } from '@ant-design/icons';
+import expandedIcon from '../../../../../public/images/treenode_open.png';
+import closeIcon from '../../../../../public/images/treenode_collapse.png';
+import { KCInput } from '@/components/KCInput';
+import { getDictByDictTypeAndSysid } from '@/service/dictionary';
+
+import {
+  getCategoryTree,
+  addCategory,
+  getProjectList,
+  getOptionList,
+  addProject,
+  delProject,
+  addOption,
+  delOption,
+  updateProjectWeight,
+  updateOptionScore,
+  CategoryNode,
+  ProjectItem,
+  OptionItem,
+  SystemTreeNode,
+  getSystemListForModal,
+  addSystems,
+  getSelectedSystemList,
+  addServiceTypes,
+  getServiceEvaluationItem,
+  getEvaluationSelect,
+  addEvaluationItem,
+  addEvaluationSelect,
+  editEvaluationSelect,
+  deleteEvaluationItem,
+  deleteEvaluationSelect,
+} from './service';
+
+const IconFont = createFromIconfontCN({
+  scriptUrl: '/zhongtaiC.js',
+});
+
+// 子表格:选项列表
+const OptionsList: React.FC<{
+  project: ProjectItem;
+  options: OptionItem[];
+  onAdd: (data: { optionCode: string; optionName: string; score: number }) => Promise<void>;
+  onDelete: (optionId: string) => Promise<void>;
+  onScoreChange: (optionId: string, score: number) => Promise<void>;
+}> = ({ project, options, onAdd, onDelete, onScoreChange }) => {
+  return (
+    <div className="SE-OptionsWrap">
+      <div className="SE-OptionsHeader">
+        <span className="SE-SubTitle">{project.projectName} - 选项</span>
+        <ModalForm
+          title="新增选项"
+          width={520}
+          trigger={<Button type="link">新增选项</Button>}
+          modalProps={{ destroyOnClose: true }}
+          onFinish={async (values) => {
+            await onAdd(values as any);
+            return true;
+          }}
+        >
+          <ProFormText name="optionCode" label="选项代码" placeholder="请输入" rules={[{ required: true, message: '必填' }]} />
+          <ProFormText name="optionName" label="选项名称" placeholder="请输入" rules={[{ required: true, message: '必填' }]} />
+          <ProFormDigit name="score" label="选项得分" placeholder="请输入" min={0} max={9999} rules={[{ required: true, message: '必填' }]} />
+        </ModalForm>
+      </div>
+
+      <div className="SE-OptionsTable">
+        <div className="SE-OptionsRow SE-OptionsHead">
+          <div className="col idx">序号</div>
+          <div className="col code">选项代码</div>
+          <div className="col name">选项名称</div>
+          <div className="col score">选项得分</div>
+          <div className="col act">操作</div>
+        </div>
+        {options && options.length > 0 ? (
+          options.map((opt, index) => (
+            <div className="SE-OptionsRow" key={opt.optionId}>
+              <div className="col idx">{index + 1}</div>
+              <div className="col code">{opt.optionCode}</div>
+              <div className="col name">{opt.optionName}</div>
+              <div className="col score">
+                <InputNumber
+                  min={0}
+                  max={9999}
+                  value={opt.score}
+                  onChange={(val) => {
+                    const v = typeof val === 'number' ? val : Number(val);
+                    if (!Number.isNaN(v)) onScoreChange(opt.optionId, v);
+                  }}
+                />
+              </div>
+              <div className="col act">
+                <Popconfirm title="是否确认删除?" onConfirm={() => onDelete(opt.optionId)}>
+                  <a>删除</a>
+                </Popconfirm>
+              </div>
+            </div>
+          ))
+        ) : (
+          <div className="SE-Empty">暂无选项</div>
+        )}
+      </div>
+    </div>
+  );
+};
+
+const ServiceEvaluatePage: React.FC = () => {
+  // 左侧分类树
+  const [categoryTree, set_categoryTree] = useState<CategoryNode[]>([]);
+  const [currentCategory, set_currentCategory] = useState<CategoryNode | undefined>();
+  const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
+  const [autoExpandParent, setAutoExpandParent] = useState(true);
+  const [searchValue, setSearchValue] = useState('');
+
+  // 主表相关
+  const [reload, set_reload] = useState(false);
+  const [tableParams, set_tableParams] = useState<{ current: number; pageSize: number; categoryId?: string }>({ current: 1, pageSize: 10 });
+  const [tableKey, set_tableKey] = useState(0); // 用于强制刷新表格
+  const [expandedRowKeys, set_expandedRowKeys] = useState<React.Key[]>([]); // 追踪展开的行
+
+  // 展开区缓存选项数据,避免频繁请求
+  const [projectOptionsMap, set_projectOptionsMap] = useState<Record<string, OptionItem[]>>({});
+  // 项目权重编辑态(保持原交互:编辑/勾选)
+  const [editingProjectId, set_editingProjectId] = useState<string | null>(null);
+  const [editingProjectWeight, set_editingProjectWeight] = useState<number>(0);
+
+  // 添加系统弹窗
+  const [sysModalOpen, set_sysModalOpen] = useState(false);
+  const [systemTree, set_systemTree] = useState<SystemTreeNode[]>([]);
+  const [sysTargetKeys, set_sysTargetKeys] = useState<string[]>([]);
+  const [sysSelectedKeys, set_sysSelectedKeys] = useState<string[]>([]);
+  const [sysExpandedKeys, set_sysExpandedKeys] = useState<string[]>([]);
+  // 服务项弹窗
+  const [serviceModalOpen, set_serviceModalOpen] = useState(false);
+  const [serviceDict, set_serviceDict] = useState<{ key: string; title: string }[]>([]);
+  const [serviceSelected, set_serviceSelected] = useState<string[]>([]);
+  const [serviceBindSystemId, set_serviceBindSystemId] = useState<string>('');
+  const [serviceSaving, set_serviceSaving] = useState(false);
+  // 服务节点展开行:缓存“评价项 -> 选项”数据(与 /centerSys/evaluation/getEvaluationSelect 返回结构一致)
+  const [serviceOptionsMap, set_serviceOptionsMap] = useState<
+    Record<string, { id: number | string; hospId?: string; itemCode: string; code: string; name: string; value: number; sort: number }[]>
+  >({});
+
+  // 评价项目弹窗(右侧“添加”)
+  const [evalItemModalOpen, set_evalItemModalOpen] = useState(false);
+  const [evalItemDict, set_evalItemDict] = useState<{ key: string; title: string }[]>([]);
+  const [evalItemTargetKeys, set_evalItemTargetKeys] = useState<string[]>([]);
+  const [evalItemSelectedKeys, set_evalItemSelectedKeys] = useState<string[]>([]);
+  // 评价项权重编辑态
+  const [editingEvalCode, set_editingEvalCode] = useState<string | null>(null);
+  const [editingEvalValue, set_editingEvalValue] = useState<number>(0);
+  // 选项弹窗(评价项 -> 选项)
+  const [selectModalOpen, set_selectModalOpen] = useState(false);
+  const [selectDict, set_selectDict] = useState<{ key: string; title: string }[]>([]);
+  const [selectTargetKeys, set_selectTargetKeys] = useState<string[]>([]);
+  const [selectSelectedKeys, set_selectSelectedKeys] = useState<string[]>([]);
+  const [bindItemCode, set_bindItemCode] = useState<string>('');
+  // 服务节点主表数据缓存(用于拖拽排序后保持展示顺序)
+  const [evalItemRows, set_evalItemRows] = useState<any[]>([]);
+  const [draggingMainKey, set_draggingMainKey] = useState<string | null>(null);
+  const [draggingOverMainKey, set_draggingOverMainKey] = useState<string | null>(null);
+  const [useLocalEvalOrder, set_useLocalEvalOrder] = useState<boolean>(false);
+  // 嵌套选项拖拽的中间态
+  const [draggingOption, set_draggingOption] = useState<{ parent: string; key: string } | null>(null);
+  const [draggingOverOption, set_draggingOverOption] = useState<{ parent: string; key: string } | null>(null);
+
+  // 工具:移动数组元素
+  const moveItem = <T,>(list: T[], from: number, to: number): T[] => {
+    const next = [...list];
+    const [m] = next.splice(from, 1);
+    next.splice(to, 0, m);
+    return next;
+  };
+
+  // 获取左侧“已选系统”并渲染为单层树
+  const fetchTree = async () => {
+    const list = await getSelectedSystemList();
+    const tree: any[] = (list || []).map((item: any) => ({
+      id: String(item.systemId),
+      name: String(item.systemName),
+      nodeType: 'system',
+      selectable: false,
+      // 将 serviceList 映射为子节点
+      children: (item.serviceList || []).map((s: any) => ({
+        id: String(s.id),
+        name: String(s.name),
+        code: String(s.code || ''),
+        nodeType: 'service',
+        selectable: true,
+      })),
+    }));
+    set_categoryTree(tree as any);
+
+    // 默认选中第一个服务;若无服务则只展开系统,不触发右侧
+    if (tree.length > 0) {
+      const sys = tree[0];
+      if (sys.children && sys.children.length > 0) {
+        const firstChild = sys.children[0];
+        set_currentCategory(firstChild as any);
+        set_tableParams((p) => ({ ...p, current: 1, categoryId: firstChild.id }));
+        set_reload(true);
+        set_editingEvalCode(null);
+        // 服务节点:首次加载右侧评价项
+        if ((firstChild as any).nodeType === 'service') {
+          await loadServiceEvalItems(String((firstChild as any).code || (firstChild as any).name));
+        }
+      }
+      if (sys.children && sys.children.length > 0) {
+        setExpandedKeys([sys.id]);
+          setAutoExpandParent(true);
+      }
+    }
+  };
+
+  // 获取系统树(弹窗数据源)
+  const fetchSystemTree = async () => {
+    const { systemList, selectSystemList } = await getSystemListForModal();
+    set_systemTree(systemList || []);
+    // 默认已选
+    set_sysTargetKeys((selectSystemList || []).map((v)=> String(v)));
+    // 默认全部展开
+    const allKeys: string[] = [];
+    const walk = (list: SystemTreeNode[]) => { list.forEach(n=>{ allKeys.push(String(n.code)); if (n.children && n.children.length) walk(n.children); }); };
+    walk(systemList || []);
+    set_sysExpandedKeys(allKeys);
+  };
+
+  // 获取服务项字典
+  const fetchServiceDict = async () => {
+    // 使用中心系统ID:1547394914533380096 获取指定字典类型
+    const resp: any = await getDictByDictTypeAndSysid('1547394914533380096', 'EVALUATION_SERVICE');
+    const list = (resp?.dataVoList || []).map((x: any) => ({ key: String(x.code || x.value), title: String(x.name) }));
+    set_serviceDict(list);
+  };
+
+  // 获取评价项目字典
+  const fetchEvaluationItemsDict = async () => {
+    const resp: any = await getDictByDictTypeAndSysid('1547394914533380096', 'EVALUATION_ITEM');
+    const list = (resp?.dataVoList || []).map((x: any) => ({ key: String(x.code || x.value), title: String(x.name) }));
+    set_evalItemDict(list);
+  };
+
+  // 获取“选项”字典
+  const fetchEvaluationSelectDict = async () => {
+    const resp: any = await getDictByDictTypeAndSysid('1547394914533380096', 'EVALUATION_SELECT');
+    const list = (resp?.dataVoList || []).map((x: any) => ({ key: String(x.code || x.value), title: String(x.name) }));
+    set_selectDict(list);
+  };
+
+  // 拉取服务类型下的评价项(用于 dataSource 渲染 & 拖拽)
+  const loadServiceEvalItems = async (serviceCode: string) => {
+    const list = (await getServiceEvaluationItem(serviceCode)) || [];
+    const normalizePercent = (v: any): number => {
+      const n = Number(v);
+      return Number.isFinite(n) && n >= 0 ? n : 0;
+    };
+    const mapped = (list || []).map((it: any, idx: number) => ({
+      id: it.id,  // 保留 id 字段,用于删除等操作
+      itemCode: it.itemCode,
+      itemName: it.itemName,
+      percent: normalizePercent(it.percent),
+      sort: it.sort ?? idx + 1,
+    }));
+    set_evalItemRows(mapped);
+    set_useLocalEvalOrder(true);
+  };
+
+  // 获取表格数据:服务节点 → 评价项;系统/分类节点 → 项目列表
+  const getTableData = async (params: any) => {
+    set_reload(false);
+    if (!currentCategory) return { data: [], success: true } as any;
+    const node: any = currentCategory as any;
+    if (node.nodeType === 'service') {
+      // 使用 dataSource 渲染时,这里返回本地 rows,避免与 KCTable 拖拽冲突
+      if (evalItemRows && evalItemRows.length > 0) {
+        return { data: evalItemRows, success: true } as any;
+      }
+      await loadServiceEvalItems(String(node.code || node.name));
+      return { data: evalItemRows, success: true } as any;
+    }
+    const resp = await getProjectList({ categoryId: currentCategory.id, ...params });
+    return { data: resp.list || [], success: true } as any;
+  };
+
+  // 主表列定义 - 项目
+  const projectColumns: ProColumns<ProjectItem>[] = useMemo(
+    () => [
+      { title: '', dataIndex: 'drag', width: 32, render: () => (<span className="SE-DragHandle" style={{ cursor:'move', display:'flex', alignItems:'center', justifyContent:'center', height:'100%' }}><IconFont type="iconliebiao" style={{ fontSize: 16, color: '#99A6BF' }} /></span>) },
+      {
+        title: '序号',
+        dataIndex: 'index',
+        width: 60,
+        render: (_, __, index) => index + 1,
+      },
+      {
+        title: '评价项目代码',
+        dataIndex: 'projectCode',
+        width: 100,
+      },
+      {
+        title: '评价项目名称',
+        dataIndex: 'projectName',
+        ellipsis: true,
+      },
+      {
+        title: '权重',
+        dataIndex: 'weight',
+        width: 120,
+        render: (_, record) => (
+          <InputNumber
+            min={0}
+            max={100}
+            formatter={(value) => `${value}%`}
+            parser={(value) => Number(String(value).replace('%', ''))}
+            value={record.weight}
+            onChange={async (val) => {
+              const weight = typeof val === 'number' ? val : Number(val);
+              if (Number.isNaN(weight)) return;
+              const ok = await updateProjectWeight(record.projectId, weight);
+              if (ok) set_reload(true);
+            }}
+          />
+        ),
+      },
+      {
+        title: '操作',
+        key: 'option',
+        width: 160,
+        valueType: 'option',
+        render: (_, record) => [
+          <ModalForm
+            key="add"
+            title={`新增“${record.projectName}”的选项`}
+            trigger={<a>选项</a>}
+            width={520}
+            onFinish={async (values) => {
+              const ok = await addOption({ projectId: record.projectId, ...(values as any) });
+              if (ok) {
+              const opts = await getOptionList({ projectId: record.projectId });
+              set_projectOptionsMap((old) => ({ ...old, [record.projectId]: opts || [] }));
+              }
+              return !!ok;
+            }}
+          >
+            <ProFormText name="optionCode" label="选项代码" rules={[{ required: true, message: '必填' }]} />
+            <ProFormText name="optionName" label="选项名称" rules={[{ required: true, message: '必填' }]} />
+            <ProFormDigit name="score" label="选项得分" min={0} max={9999} rules={[{ required: true, message: '必填' }]} />
+          </ModalForm>,
+          <Popconfirm key="del" title="是否确认删除?" onConfirm={async () => { const ok = await delProject(record.projectId); if (ok) set_reload(true); }}>
+            <a>删除</a>
+          </Popconfirm>,
+        ],
+      },
+    ],
+    [projectOptionsMap]
+  );
+
+  // 评价项表格列
+  const evalItemColumns: ProColumns<any>[] = useMemo(
+    () => [
+      { title: '', dataIndex: 'drag', width: 32, render: () => (<span className="SE-DragHandle" style={{ cursor:'move', display:'flex', alignItems:'center', justifyContent:'center', height:'100%' }}><IconFont type="iconliebiao" style={{ fontSize: 16, color: '#99A6BF' }} /></span>) },
+      { title: '序号', dataIndex: 'index', width: 60, render: (_, __, index) => index + 1 },
+      { title: '评价项目代码', dataIndex: 'itemCode', width: 120 },
+      { title: '评价项目名称', dataIndex: 'itemName', ellipsis: true },
+      {
+        title: '权重',
+        dataIndex: 'percent',
+        width: 120,
+        render: (v: any) => `${(Number.isFinite(Number(v)) && Number(v) >= 0 ? Number(v) : 0)}%`,
+      },
+      // 移除不需要的排序列
+      {
+        title: '操作',
+        valueType: 'option',
+        width: 120,
+        render: (_, record) => [
+          <a key="opt" onClick={async () => { 
+            set_bindItemCode(record.itemCode); 
+            // 获取该评价项已有的选项
+            const existingOptions = await getEvaluationSelect(record.itemCode);
+            const existingKeys = (existingOptions || []).map((item: any) => String(item.code));
+            set_selectTargetKeys(existingKeys); 
+            set_selectSelectedKeys([]); 
+            await fetchEvaluationSelectDict(); 
+            set_selectModalOpen(true); 
+          }}>选项</a>,
+          <Popconfirm key="del" title="是否确认删除?" onConfirm={async () => { 
+            const res = await deleteEvaluationItem(record.id);
+            if (res !== false) {
+              // 删除成功后,如果该项是展开状态,从展开列表中移除
+              set_expandedRowKeys((prev) => prev.filter(k => k !== record.itemCode));
+              // 重新加载评价项列表
+              const current: any = currentCategory as any;
+              if (current && current.nodeType === 'service') {
+                await loadServiceEvalItems(String(current.code || current.name));
+              }
+            }
+          }}>
+            <a>删除</a>
+          </Popconfirm>,
+        ],
+      },
+    ],
+    [editingEvalCode, editingEvalValue]
+  );
+
+  // 展开行渲染(分类/系统场景使用)
+  const expandedRowRender = (record: ProjectItem) => {
+    const options = projectOptionsMap[record.projectId] || [];
+    return (
+      <OptionsList
+        project={record}
+        options={options}
+        onAdd={async (data) => {
+          const ok = await addOption({ projectId: record.projectId, ...data });
+          if (ok) {
+          const opts = await getOptionList({ projectId: record.projectId });
+          set_projectOptionsMap((old) => ({ ...old, [record.projectId]: opts || [] }));
+          }
+        }}
+        onDelete={async (optionId) => {
+          const ok = await delOption(optionId);
+          if (ok) {
+          const opts = await getOptionList({ projectId: record.projectId });
+          set_projectOptionsMap((old) => ({ ...old, [record.projectId]: opts || [] }));
+          }
+        }}
+        onScoreChange={async (optionId, score) => {
+          const ok = await updateOptionScore(optionId, score);
+          if (ok) {
+          const opts = await getOptionList({ projectId: record.projectId });
+          set_projectOptionsMap((old) => ({ ...old, [record.projectId]: opts || [] }));
+          }
+        }}
+      />
+    );
+  };
+
+  // 服务节点:展开渲染(与外层保持一致的 KCTable 风格)
+  const expandedRowRenderService = (record: any) => {
+    const parentCode: string = record.itemCode;
+    const rows = serviceOptionsMap[parentCode] || [];
+    const columns: ProColumns<any>[] = [
+      {
+        title: '',
+        dataIndex: 'drag',
+        width: 32,
+        render: (_, row: any) => (
+          <span
+            className="SE-DragHandle"
+            style={{ cursor: 'move', display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}
+            draggable
+            onDragStart={(e) => {
+              set_draggingOption({ parent: parentCode, key: String(row.id ?? row.code) });
+              e.dataTransfer.effectAllowed = 'move';
+              try { e.dataTransfer.setData('text/plain', String(row.id ?? row.code)); } catch {}
+            }}
+            onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }}
+            onDrop={(e) => {
+              e.preventDefault();
+              if (!draggingOption || draggingOption.parent !== parentCode) return;
+              const list = serviceOptionsMap[parentCode] || [];
+              const from = list.findIndex((r: any) => String(r.id ?? r.code) === draggingOption.key);
+              const to = list.findIndex((r: any) => String(r.id ?? r.code) === String(row.id ?? row.code));
+              if (from === -1 || to === -1 || from === to) return;
+              const next = moveItem(list, from, to);
+              set_serviceOptionsMap((old) => ({ ...old, [parentCode]: next }));
+              set_draggingOption(null);
+              set_reload((v)=>!v);
+              // TODO: 保存选项排序
+            }}
+            title="拖动排序"
+          >
+            <IconFont type="iconliebiao" style={{ fontSize: 16, color: '#99A6BF' }} />
+          </span>
+        ),
+      },
+      { title: '序号', dataIndex: 'index', width: 60, render: (_, __, index) => index + 1 },
+      { title: '选项代码', dataIndex: 'code', width: 120 },
+      { title: '选项名称', dataIndex: 'name', ellipsis: true },
+      {
+        title: '选项得分',
+        dataIndex: 'value',
+        width: 200,
+        render: (v: any, row: any) => {
+          const isEditing = row.__editing === true;
+          if (!isEditing) {
+    return (
+              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+                <div style={{ padding: '2px 10px', border: '1px solid #dae2f2', borderRadius: 6, minWidth: 72, textAlign: 'center', background: '#f7f9fc' }}>{v ?? 0}</div>
+                <span
+                  onClick={() => {
+                    set_serviceOptionsMap((old) => {
+                      const list = [...(old[parentCode] || [])];
+                      const idx = list.findIndex((i: any) => String(i.id ?? i.code) === String(row.id ?? row.code));
+                      if (idx > -1) list[idx] = { ...list[idx], __editing: true } as any;
+                      return { ...old, [parentCode]: list };
+                    });
+                  }}
+                  style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', height: 28, width: 28, border: '1px solid #dae2f2', borderRadius: 6, cursor: 'pointer' }}
+                >
+                  <IconFont type="iconbianji" style={{ fontSize: 16, color: '#17181A' }} />
+                </span>
+          </div>
+            );
+          }
+          return (
+            <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+              <InputNumber
+                min={0}
+                max={9999}
+                value={row.value}
+                onChange={(val) => {
+                  const next = Number(val) || 0;
+                  set_serviceOptionsMap((old) => {
+                    const list = [...(old[parentCode] || [])];
+                    const idx = list.findIndex((i: any) => String(i.id ?? i.code) === String(row.id ?? row.code));
+                    if (idx > -1) list[idx] = { ...list[idx], value: next, __editing: true } as any;
+                    return { ...old, [parentCode]: list };
+                  });
+                }}
+                style={{ width: 100 }}
+              />
+              <span
+                onClick={async () => {
+                  // 调用接口保存选项分值
+                  const res = await editEvaluationSelect(row.id, row.value);
+                  // 响应拦截器:成功返回 data 或 true,失败返回 false
+                  // 且 POST 成功时拦截器已自动显示"操作成功!"提示
+                  if (res !== false) {
+                    // 成功,退出编辑状态
+                    set_serviceOptionsMap((old) => {
+                      const list = [...(old[parentCode] || [])];
+                      const idx = list.findIndex((i: any) => String(i.id ?? i.code) === String(row.id ?? row.code));
+                      if (idx > -1) list[idx] = { ...list[idx], __editing: false } as any;
+                      return { ...old, [parentCode]: list };
+                    });
+                  }
+                  // 失败时拦截器已显示错误提示,这里只需保持编辑状态
+                }}
+                style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', height: 28, width: 28, border: '1px solid #dae2f2', borderRadius: 6, cursor: 'pointer' }}
+              >
+                <IconFont type="iconqueren" style={{ fontSize: 16, color: '#17181A' }} />
+              </span>
+              </div>
+          );
+        },
+      },
+      {
+        title: '操作',
+        valueType: 'option',
+        width: 120,
+        render: (_, row: any) => [
+          <Popconfirm 
+            key="del" 
+            title="是否确认删除?" 
+            onConfirm={async () => {
+              const res = await deleteEvaluationSelect(row.id);
+              if (res !== false) {
+                // 删除成功,重新加载该评价项的选项列表
+                const list = await getEvaluationSelect(parentCode);
+                set_serviceOptionsMap((old) => ({
+                  ...old,
+                  [parentCode]: (list || []) as any,
+                }));
+              }
+            }}
+          >
+            <a>删除</a>
+          </Popconfirm>
+        ],
+      },
+    ];
+
+    return (
+      <div className="SE-ExpandWrap">
+        <KCTable
+          newVer
+          columns={columns as ProColumns[]}
+          dataSource={rows}
+          rowKey={(r: any) => String(r.id ?? r.code)}
+          dragSortKey="id"
+          dragType="KC_TABLE_NESTED_ROW"
+          pagination={false}
+          toolBarRender={false as any}
+          search={false as any}
+          options={false as any}
+          bordered={false as any}
+          size={'small' as any}
+          dragSort
+          onDragSortEnd={(next)=>{ 
+            set_serviceOptionsMap((old)=> ({ ...old, [parentCode]: next })); 
+          }}
+        />
+      </div>
+    );
+  };
+
+  // 表格展开回调
+  const onExpand = async (expanded: boolean, record: any) => {
+    const node: any = currentCategory as any;
+    const rowKey = node?.nodeType === 'service' ? record.itemCode : record.projectId;
+    
+    // 更新展开行状态
+    if (expanded) {
+      set_expandedRowKeys((prev) => [...prev, rowKey]);
+    } else {
+      set_expandedRowKeys((prev) => prev.filter(k => k !== rowKey));
+      return;
+    }
+    
+    // 加载数据
+    if (node && node.nodeType === 'service') {
+      const list = await getEvaluationSelect(record.itemCode);
+      set_serviceOptionsMap((old: Record<string, { id: number | string; hospId?: string; itemCode: string; code: string; name: string; value: number; sort: number }[]>) => ({
+        ...old,
+        [record.itemCode]: (list || []) as { id: number | string; hospId?: string; itemCode: string; code: string; name: string; value: number; sort: number }[],
+      }));
+    } else {
+      const opts = await getOptionList({ projectId: record.projectId });
+      set_projectOptionsMap((old) => ({ ...old, [record.projectId]: opts || [] }));
+    }
+  };
+  
+  // 收起所有展开行
+  const collapseAllRows = () => {
+    set_expandedRowKeys([]);
+  };
+
+  useEffect(() => { fetchTree(); }, []);
+  useEffect(() => { if (sysModalOpen) fetchSystemTree(); }, [sysModalOpen]);
+  useEffect(() => {
+    if (currentCategory) {
+      set_tableParams((p) => ({ ...p, current: 1, categoryId: currentCategory.id }));
+      set_reload(true);
+      // 切换分类时清空展开行
+      set_expandedRowKeys([]);
+    }
+  }, [currentCategory]);
+
+  return (
+    <>
+    <div className="SystemNavMana">
+      <div className="leftTree">
+        <div className="search" style={{display:'flex',gap:8,alignItems:'center'}}>
+          <Input className="searchInput" placeholder="请输入" size="small" allowClear onChange={(e)=>setSearchValue(e.target.value)} />
+          <Tooltip title="添加系统">
+            <span className="add" onClick={()=> set_sysModalOpen(true)}>
+              <IconFont style={{ color: '#17181A', fontSize: 12 }} type="icon-xinzeng" />
+            </span>
+          </Tooltip>
+        </div>
+        <div className="treeeWrap">
+          {categoryTree && categoryTree.length>0 && (
+            <DirectoryTree
+              fieldNames={{ title:'name', key:'id' }}
+              rootStyle={{ overflowY:'scroll', overflowX:'hidden' }}
+              onSelect={async (keys, info)=> {
+                const node = info.node as any;
+                // 仅允许选择服务节点
+                if (node.nodeType !== 'service') return;
+                set_currentCategory(node);
+                set_tableParams((p)=>({ ...p, current:1, categoryId: node.id }));
+                set_reload(true);
+                // 服务节点:拉取右侧评价项(dataSource 渲染)
+                await loadServiceEvalItems(String(node.code || node.name));
+              }}
+              onExpand={(keys)=>{ setExpandedKeys(keys as React.Key[]); setAutoExpandParent(false);} }
+              expandedKeys={expandedKeys}
+              autoExpandParent={autoExpandParent}
+              selectedKeys={currentCategory && (currentCategory as any).nodeType==='service' ? [currentCategory.id] : []}
+              blockNode
+              className="KC-DirectoryTree"
+              icon={() => null}
+              titleRender={(nodeData: any) => {
+                const strTitle = nodeData.name as string;
+                const index = strTitle.indexOf(searchValue);
+                const beforeStr = strTitle.substring(0, index);
+                const afterStr = strTitle.slice(index + searchValue.length);
+                const title = index > -1 ? (
+                  <span>
+                    {beforeStr}
+                    <span className="site-tree-search-value" style={{ color: 'red', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{searchValue}</span>
+                    {afterStr}
+                  </span>
+                ) : (
+                  <span className="strTitle">{strTitle}</span>
+                );
+                const isDirectory = Array.isArray(nodeData.children) && nodeData.children.length > 0;
+                return (
+                  <div style={{display:'flex',alignItems:'center',height:32}}>
+                    <span className="node-title-area" style={{flex:1, minWidth:0}}>{title}</span>
+                    {nodeData.nodeType === 'system' && (
+                      <span
+                        className="inline-add"
+                        style={{display:'inline-flex', justifyContent:'center', alignItems:'center'}}
+                        onClick={async (e)=>{
+                          e.stopPropagation();
+                        set_serviceBindSystemId(String(nodeData.id));
+                        // 打开前重置已选,避免上次选择残留
+                        set_serviceSelected([]);
+                        await fetchServiceDict();
+                        set_serviceModalOpen(true);
+                        }}
+                      >
+                        <IconFont style={{ color: '#99A6BF', fontSize: 12 }} type="icon-xinzeng" />
+                      </span>
+                    )}
+                  </div>
+                );
+              }}
+              treeData={categoryTree as unknown as DataNode[]}
+              switcherIcon={(props:any)=> props.expanded ? (<img style={{ width:20,height:20, position:'relative', top:5 }} src={closeIcon} />) : (<img style={{ width:20,height:20, position:'relative', top:5 }} src={expandedIcon} />)}
+            />
+          )}
+        </div>
+      </div>
+
+      <div className="rightContent">
+        <div className="tableToolbar">
+          <div className="filter">
+            <span style={{ fontSize: 16, fontWeight: 500, color: '#17181A' }}>
+              {currentCategory?.name || '-'}
+            </span>
+          </div>
+          <div className={'btnGroup'}>
+            {expandedRowKeys.length > 0 && (
+              <span className="collapse" onClick={collapseAllRows}>
+                收起所有 ({expandedRowKeys.length})
+              </span>
+            )}
+            <span className="add" onClick={async ()=>{ 
+              // 获取当前已有的评价项,用于回显
+              const current: any = currentCategory as any;
+              if (current && current.nodeType === 'service') {
+                const existingItems = await getServiceEvaluationItem(String(current.code || current.name));
+                const existingKeys = (existingItems || []).map((item: any) => String(item.itemCode));
+                set_evalItemTargetKeys(existingKeys);
+              } else {
+                set_evalItemTargetKeys([]);
+              }
+              set_evalItemSelectedKeys([]); 
+              await fetchEvaluationItemsDict(); 
+              set_evalItemModalOpen(true); 
+            }}>
+              添加
+            </span>
+          </div>
+          <ModalForm
+            title="新增评价项目"
+            width={520}
+            trigger={<span style={{ display:'none' }} />}
+            onFinish={async (values) => {
+              if (!currentCategory) return false;
+              const ok = await addProject({ categoryId: currentCategory.id, ...(values as any) });
+              if (ok) set_reload(true);
+              return !!ok;
+            }}
+          >
+            <ProFormText name="projectCode" label="项目代码" placeholder="请输入" rules={[{ required: true, message: '必填' }]} />
+            <ProFormText name="projectName" label="项目名称" placeholder="请输入" rules={[{ required: true, message: '必填' }]} />
+            <ProFormDigit name="weight" label="权重%" min={0} max={100} placeholder="0-100" rules={[{ required: true, message: '必填' }]} />
+          </ModalForm>
+        </div>
+
+        {currentCategory && (
+          <KCTable
+            key={`table-${currentCategory.id}`}
+            newVer
+            reload={(currentCategory as any)?.nodeType === 'service' ? false : reload}
+            params={(currentCategory as any)?.nodeType === 'service' ? undefined : tableParams}
+            rowKey={(((currentCategory as any)?.nodeType === 'service')
+              ? ((r: any) => String(r?.itemCode ?? ''))
+              : ((r: any) => String(r?.projectId ?? ''))) as any}
+            dragSortKey={(currentCategory as any)?.nodeType === 'service' ? 'itemCode' : 'projectId'}
+            dragType="KC_TABLE_MAIN_ROW"
+            disabledDragKeys={expandedRowKeys}
+            scroll={{ y: `calc(100vh - 240px)` }}
+            columns={((currentCategory as any)?.nodeType === 'service' ? evalItemColumns : projectColumns) as ProColumns[]}
+            request={(((currentCategory as any)?.nodeType === 'service') ? undefined : ((p: any, s: any, f: any) => getTableData(p))) as any}
+            expandable={{
+              expandedRowRender: (currentCategory as any)?.nodeType === 'service' ? expandedRowRenderService : expandedRowRender,
+              onExpand,
+              expandedRowKeys,
+              onExpandedRowsChange: (keys: readonly React.Key[]) => set_expandedRowKeys([...keys]),
+              expandIconColumnIndex: 1  // 展开图标显示在第2列,第1列留给拖拽手柄
+            }}
+            pagination={false}
+            dragSort={(currentCategory as any)?.nodeType === 'service'}
+            dataSource={((currentCategory as any)?.nodeType === 'service') ? evalItemRows : undefined}
+            onDragSortEnd={(rows: any[])=>{ 
+              set_evalItemRows(rows); 
+              set_useLocalEvalOrder(true);
+            }}
+          />
+        )}
+      </div>
+    </div>
+    <Modal
+      title="添加系统"
+      open={sysModalOpen}
+      onCancel={()=> set_sysModalOpen(false)}
+      onOk={async ()=>{
+        const flat = (nodes: SystemTreeNode[]): SystemTreeNode[] => nodes.reduce<SystemTreeNode[]>((acc, n)=>{
+          acc.push(n);
+          if (n.children && n.children.length) acc.push(...flat(n.children));
+          return acc;
+        }, []);
+        const all = flat(systemTree);
+        const payload = all
+          .filter(n => sysTargetKeys.includes(n.code) && n.type === 3)
+          .map(n => ({ systemId: n.code, systemName: n.name }));
+        if (payload.length === 0) {
+          message.warning('请选择系统');
+          return;
+        }
+        const ok = await addSystems(payload as any);
+        if (ok) {
+          set_sysModalOpen(false);
+          // 刷新左侧“已选系统”列表
+          set_reload(true);
+          fetchTree();
+        }
+      }}
+      width={720}
+      destroyOnClose
+    >
+      <Transfer
+        className="tree-transfer"
+        listStyle={{ height: 540 }}
+        dataSource={(function buildFlat(){
+          const res: any[] = [];
+          const walk = (list: SystemTreeNode[], parentChecked: boolean)=>{
+            list.forEach(n=>{
+              res.push({ key: n.code, code: n.code, name: n.name, type: n.type, node: n });
+              if (n.children && n.children.length) walk(n.children, false);
+            })
+          };
+          walk(systemTree, false);
+          return res;
+        })()}
+        rowKey={(item)=> item.code}
+        titles={[`待选项`, `已选项`]}
+        targetKeys={sysTargetKeys}
+        selectedKeys={sysSelectedKeys}
+        showSearch
+        locale={{ itemUnit:'项', itemsUnit:'项', searchPlaceholder:'请输入' }}
+        render={(item)=> item.name}
+        onChange={(nextTargetKeys)=> set_sysTargetKeys(nextTargetKeys as string[])}
+        onSelectChange={(sourceSelectedKeys, targetSelectedKeys)=> set_sysSelectedKeys([...(sourceSelectedKeys as string[]), ...(targetSelectedKeys as string[])])}
+      >
+        {({ direction, onItemSelect, onItemSelectAll, selectedKeys })=>{
+          if (direction === 'left') {
+            const treeData = (function toTree(list: SystemTreeNode[]): any[] {
+              return list.map((n: SystemTreeNode): any => ({
+                key: n.code,
+                title: n.name,
+                // 不再用 children 判断是否可勾选;统一允许勾选,由 onCheck 决定加入哪些 key
+                disabled: false,
+                children: n.children ? (toTree(n.children) as any[]) : undefined,
+              }));
+            })(systemTree);
+
+            const checkedKeys = [...selectedKeys, ...sysTargetKeys];
+            return (
+              <Tree
+                checkable
+                defaultExpandAll
+                checkedKeys={checkedKeys}
+                treeData={treeData as any}
+                height={480}
+                expandedKeys={sysExpandedKeys}
+                onExpand={(keys)=> set_sysExpandedKeys(keys as string[])}
+                onCheck={(keys, { node })=>{
+                  // 仅加入“系统叶子”:type==3,或 type==5 但无 children(后端把系统标成5时兜底)
+                  const isSystemLeafByCode = (code: string) => {
+                    const flat = (function flat(){
+                      const res: any[] = [];
+                      const walk = (list: SystemTreeNode[])=>{ list.forEach(nn=>{ res.push({ code: nn.code, type: nn.type, children: nn.children }); if(nn.children) walk(nn.children); }); };
+                      walk(systemTree); return res;
+                    })();
+                    const found = flat.find(i=> i.code===code);
+                    if (!found) return false;
+                    const noChildren = !found.children || found.children.length===0;
+                    return found.type===3 || (found.type===5 && noChildren);
+                  };
+
+                  const getChildCodes = (n:any): string[] => {
+                    if (!n.children || n.children.length===0) return [n.key as string];
+                    return n.children.reduce((acc:any,c:any)=> acc.concat(getChildCodes(c)), []);
+                  };
+
+                  const isSelect = !(node as any).checked;
+                  const candidate = (node.children && node.children.length>0) ? getChildCodes(node) : [node.key as string];
+                  const systemLeafKeys = candidate.filter((code)=> isSystemLeafByCode(code));
+                  onItemSelectAll(systemLeafKeys as string[], isSelect);
+                }}
+              />
+            );
+          }
+          return null;
+        }}
+      </Transfer>
+    </Modal>
+    <Modal
+      title="添加服务"
+      open={serviceModalOpen}
+      onCancel={()=> { set_serviceModalOpen(false); set_serviceSelected([]); set_serviceBindSystemId(''); }}
+      confirmLoading={serviceSaving}
+      onOk={async ()=> {
+        if (!serviceBindSystemId) { set_serviceModalOpen(false); return; }
+        try {
+          set_serviceSaving(true);
+          const ok: any = await addServiceTypes(serviceBindSystemId, serviceSelected);
+          if (ok) {
+            set_serviceModalOpen(false);
+            set_serviceSelected([]);
+            set_serviceBindSystemId('');
+            // 刷新左侧列表
+            set_reload(true);
+            fetchTree();
+          }
+        } finally {
+          set_serviceSaving(false);
+        }
+      }}
+      width={560}
+      destroyOnClose
+    >
+      <Transfer
+        className="ServiceTransfer"
+        style={{ width: '100%' }}
+        titles={[`待选项`,`已选项`]}
+        dataSource={serviceDict}
+        rowKey={(item)=> item.key}
+        render={(item)=> item.title}
+        targetKeys={serviceSelected}
+        onChange={(next)=> set_serviceSelected(next as string[])}
+        listStyle={{ height: 460, width: 'calc(50% - 32px)' }}
+        operationStyle={{ margin: '0 12px', display: 'flex', alignItems: 'center' }}
+        locale={{ itemUnit:'项', itemsUnit:'项', searchPlaceholder:'请输入' }}
+        showSearch
+      />
+    </Modal>
+    <Modal
+      title="评价项目"
+      open={evalItemModalOpen}
+      onCancel={()=> { set_evalItemModalOpen(false); set_evalItemTargetKeys([]); set_evalItemSelectedKeys([]); }}
+      onOk={async ()=> {
+        // 依据当前选中的服务类型保存评价项目
+        const current: any = currentCategory as any;
+        if (!current || current.nodeType !== 'service') { set_evalItemModalOpen(false); return; }
+        // 从字典中匹配详细信息
+        const itemCodes = evalItemTargetKeys.map((k, idx)=>{
+          const found = evalItemDict.find(i=> i.key === k);
+          return { itemCode: k, itemName: found?.title || k, itemValue: 1, sort: idx + 1 };
+        });
+        const ok: any = await addEvaluationItem(String(current.code || current.name), itemCodes);
+        if (ok) {
+          set_evalItemModalOpen(false);
+          set_evalItemTargetKeys([]);
+          set_evalItemSelectedKeys([]);
+          // 刷新右侧表格:重新加载评价项列表
+          await loadServiceEvalItems(String(current.code || current.name));
+        }
+      }}
+      width={560}
+      destroyOnClose
+    >
+      <Transfer
+        titles={[`待选项`,`已选项`]}
+        dataSource={evalItemDict}
+        rowKey={(item)=> item.key}
+        render={(item)=> item.title}
+        targetKeys={evalItemTargetKeys}
+        selectedKeys={evalItemSelectedKeys}
+        onChange={(next)=> set_evalItemTargetKeys(next as string[])}
+        onSelectChange={(l,r)=> set_evalItemSelectedKeys([...(l as string[]), ...(r as string[])])}
+        listStyle={{ height: 460, width: 'calc(50% - 32px)' }}
+        operationStyle={{ margin: '0 12px', display: 'flex', alignItems: 'center' }}
+        showSearch
+        locale={{ itemUnit:'项', itemsUnit:'项', searchPlaceholder:'请输入' }}
+      />
+    </Modal>
+    <Modal
+      title="添加选项"
+      open={selectModalOpen}
+      onCancel={()=> { set_selectModalOpen(false); set_selectTargetKeys([]); set_selectSelectedKeys([]); set_bindItemCode(''); }}
+      onOk={async ()=> {
+        const selectCodes = selectTargetKeys.map((k, idx)=>{
+          const found = selectDict.find(i=> i.key === k);
+          return { itemCode: k, itemName: found?.title || k, itemValue: 1, sort: idx + 1 };
+        });
+        const ok: any = await addEvaluationSelect(bindItemCode, selectCodes);
+        if (ok !== false) {
+          set_selectModalOpen(false);
+          set_selectTargetKeys([]);
+          set_selectSelectedKeys([]);
+          // 重新加载该评价项的选项数据
+          const list = await getEvaluationSelect(bindItemCode);
+          set_serviceOptionsMap((old) => ({
+            ...old,
+            [bindItemCode]: (list || []) as any,
+          }));
+          set_bindItemCode('');
+          set_reload(true);
+        }
+      }}
+      width={560}
+      destroyOnClose
+    >
+      <Transfer
+        titles={[`待选项`,`已选项`]}
+        dataSource={selectDict}
+        rowKey={(item)=> item.key}
+        render={(item)=> item.title}
+        targetKeys={selectTargetKeys}
+        selectedKeys={selectSelectedKeys}
+        onChange={(next)=> set_selectTargetKeys(next as string[])}
+        onSelectChange={(l,r)=> set_selectSelectedKeys([...(l as string[]), ...(r as string[])])}
+        listStyle={{ height: 360, width: 'calc(50% - 32px)' }}
+        operationStyle={{ margin: '0 12px', display: 'flex', alignItems: 'center' }}
+        showSearch
+        locale={{ itemUnit:'项', itemsUnit:'项', searchPlaceholder:'请输入' }}
+      />
+    </Modal>
+  </>
+  );
+};
+
+export default ServiceEvaluatePage;
+
+

+ 302 - 0
src/pages/platform/setting/serviceEvaluate/service.ts

@@ -0,0 +1,302 @@
+/*
+ * @Author: code4eat awesomedema@gmail.com
+ * @Date: 2025-09-30 00:00:00
+ * @LastEditors: code4eat awesomedema@gmail.com
+ * @LastEditTime: 2025-09-30 00:00:00
+ * @FilePath: /KC-MiddlePlatform/src/pages/platform/setting/serviceEvaluate/service.ts
+ * @Description: 服务评价管理 - 服务层封装(默认使用 mock 接口)
+ */
+
+// 本地模拟返回:使用前端内存数据替代实际请求,方便在后端未就绪时开发、联调
+import { request } from 'umi';
+
+// 分类树节点
+export type CategoryNode = {
+  id: string;
+  name: string;
+  children?: CategoryNode[];
+};
+
+// 评价项目
+export type ProjectItem = {
+  projectId: string;
+  projectCode: string;
+  projectName: string;
+  weight: number; // 0-100
+};
+
+// 选项
+export type OptionItem = {
+  optionId: string;
+  optionCode: string;
+  optionName: string;
+  score: number;
+};
+
+// 分页响应(为兼容 KCTable 的 request,保持 list 字段)
+export type ListResp<T> = {
+  list: T[];
+};
+
+// ------------------ 真实接口:系统树获取 ------------------
+// 注意:页面内仍使用本地 mock 的分类树/项目/选项。
+// 本方法用于“添加系统”弹窗的数据源,请求服务端返回的 systemList 结构。
+export type SystemTreeNode = {
+  code: string;
+  name: string;
+  sort?: number;
+  type?: number;
+  children?: SystemTreeNode[];
+};
+
+export type GetSystemListResp = { systemList: SystemTreeNode[]; selectSystemList: (string | number)[] };
+
+export const getSystemListForModal = async (): Promise<GetSystemListResp> => {
+  const resp = await request<any>(
+    '/centerSys/evaluation/getSystemList',
+    { method: 'GET' },
+  );
+  // 后端返回形如 { status, msg, data: { systemList, selectSystemList } }
+  const data = resp?.data || resp || {};
+  const selectedRaw = data.selectSystemList || [];
+  const normalizeKey = (x: any) => String((x && (x.systemId || x.code)) || x);
+  const selected = Array.isArray(selectedRaw) ? selectedRaw.map((x: any) => normalizeKey(x)) : [];
+  return {
+    systemList: data.systemList || [],
+    selectSystemList: selected,
+  };
+};
+
+// 保存所选系统
+export const addSystems = async (
+  data: { systemId: string | number; systemName: string }[],
+) => {
+  return request('/centerSys/evaluation/addSystem', {
+    method: 'POST',
+    data,
+  });
+};
+
+// 获取左侧“已选系统”列表
+export type SelectedSystemItem = { id: string | number; hospId: string | number; systemId: string | number; systemName: string };
+export const getSelectedSystemList = async (): Promise<SelectedSystemItem[]> => {
+  const resp = await request<any>('/centerSys/evaluation/getSelectSystemList', { method: 'GET' });
+  // 兼容后端返回 { data: [...] }
+  return resp?.data || resp || [];
+};
+
+// 在系统下添加服务类型
+export const addServiceTypes = async (systemId: string | number, serviceTypes: (string | number)[]) => {
+  return request('/centerSys/evaluation/addService', {
+    method: 'POST',
+    data: { systemId, serviceTypes },
+  });
+};
+
+// 根据服务类型获取评价项列表
+export const getServiceEvaluationItem = async (serviceType: string) => {
+  return request<any>('/centerSys/evaluation/getServiceEvaluationItem', {
+    method: 'GET',
+    params: { serviceType },
+  });
+};
+
+// 获取右侧表格:评价项目列表(按 ItemCode)
+export const getEvaluationSelect = async (ItemCode: string) => {
+  return request<any>('/centerSys/evaluation/getEvaluationSelect', {
+    method: 'GET',
+    params: { ItemCode },
+  });
+};
+
+// 保存评价项目
+export const addEvaluationItem = async (
+  serviceType: string,
+  itemCodes: { itemCode: string; itemName: string; itemValue: number; sort: number }[],
+) => {
+  return request('/centerSys/evaluation/addEvaluationItem', {
+    method: 'POST',
+    data: { serviceType, itemCodes },
+  });
+};
+
+// 保存评价项的"选项"
+export const addEvaluationSelect = async (
+  itemCode: string,
+  selectCodes: { itemCode: string; itemName: string; itemValue: number; sort: number }[],
+) => {
+  return request('/centerSys/evaluation/addEvaluationSelect', {
+    method: 'POST',
+    data: { itemCode, selectCodes },
+  });
+};
+
+// 编辑选项得分
+export const editEvaluationSelect = async (id: number | string, value: number) => {
+  return request('/centerSys/evaluation/editEvaluationSelect', {
+    method: 'POST',
+    data: { id, value },
+  });
+};
+
+// 删除评价项目
+export const deleteEvaluationItem = async (id: number | string) => {
+  return request(`/centerSys/evaluation/deleteEvaluationItem?id=${id}`, {
+    method: 'POST',
+  });
+};
+
+// 删除选项
+export const deleteEvaluationSelect = async (id: number | string) => {
+  return request(`/centerSys/evaluation/deleteEvaluationSelect?id=${id}`, {
+    method: 'POST',
+  });
+};
+
+// 获取分类树
+// 内存数据:分类树
+const __categoryTree__: CategoryNode[] = [
+  {
+    id: 'park-app',
+    name: '停车服务APP',
+    children: [
+      { id: 'park-service', name: '停车服务', children: [] },
+      { id: 'guard-service', name: '护工服务', children: [] },
+      { id: 'food-app', name: '食堂点餐APP', children: [] },
+    ],
+  },
+];
+
+// 内存数据:项目、选项
+const __projects__: Record<string, ProjectItem[]> = {
+  'park-service': [
+    { projectId: 'p1', projectCode: 'P0001', projectName: '专业能力', weight: 25 },
+    { projectId: 'p2', projectCode: 'P0002', projectName: '服务态度', weight: 20 },
+  ],
+};
+
+const __options__: Record<string, OptionItem[]> = {
+  p1: [
+    { optionId: 'o11', optionCode: 'X0006', optionName: '1星', score: 100 },
+    { optionId: 'o12', optionCode: 'X0007', optionName: '2星', score: 2 },
+    { optionId: 'o13', optionCode: 'X0008', optionName: '3星', score: 3 },
+    { optionId: 'o14', optionCode: 'X0009', optionName: '4星', score: 4 },
+    { optionId: 'o15', optionCode: 'X0010', optionName: '5星', score: 5 },
+  ],
+  p2: [],
+};
+
+// 工具:模拟异步
+const delay = (ms = 200) => new Promise((r) => setTimeout(r, ms));
+
+export const getCategoryTree = async () => {
+  await delay();
+  return __categoryTree__;
+};
+
+// 新增分类节点(默认添加到指定父节点 children 下;未传 parentId 则添加到根)
+export const addCategory = async (parentId: string | undefined, name: string) => {
+  await delay();
+  const newNode: CategoryNode = { id: `cat_${Date.now()}`, name, children: [] };
+
+  if (!parentId) {
+    __categoryTree__.push(newNode);
+    return newNode;
+  }
+
+  // 递归查找父节点
+  const dfs = (list: CategoryNode[]): boolean => {
+    for (const node of list) {
+      if (node.id === parentId) {
+        node.children = node.children || [];
+        node.children.push(newNode);
+        return true;
+      }
+      if (node.children && node.children.length > 0) {
+        if (dfs(node.children)) return true;
+      }
+    }
+    return false;
+  };
+
+  dfs(__categoryTree__);
+  return newNode;
+};
+
+// 获取项目列表
+export const getProjectList = async (params: { categoryId: string; current?: number; pageSize?: number }) => {
+  await delay();
+  const list = __projects__[params.categoryId] || [];
+  return { list };
+};
+
+// 新增项目
+export const addProject = async (data: { categoryId: string; projectCode: string; projectName: string; weight: number }) => {
+  await delay();
+  const newItem: ProjectItem = {
+    projectId: `p_${Date.now()}`,
+    projectCode: data.projectCode,
+    projectName: data.projectName,
+    weight: Number(data.weight) || 0,
+  };
+  __projects__[data.categoryId] = __projects__[data.categoryId] || [];
+  __projects__[data.categoryId].push(newItem);
+  return true as unknown as any;
+};
+
+// 删除项目
+export const delProject = async (projectId: string) => {
+  await delay();
+  Object.keys(__projects__).forEach((k) => {
+    __projects__[k] = (__projects__[k] || []).filter((p) => p.projectId !== projectId);
+  });
+  delete __options__[projectId];
+  return true as unknown as any;
+};
+
+// 更新项目权重
+export const updateProjectWeight = async (projectId: string, weight: number) => {
+  await delay();
+  Object.keys(__projects__).forEach((k) => {
+    (__projects__[k] || []).forEach((p) => {
+      if (p.projectId === projectId) p.weight = Number(weight) || 0;
+    });
+  });
+  return true as unknown as any;
+};
+
+// 获取选项列表
+export const getOptionList = async (params: { projectId: string }) => {
+  await delay();
+  return __options__[params.projectId] || [];
+};
+
+// 新增选项
+export const addOption = async (data: { projectId: string; optionCode: string; optionName: string; score: number }) => {
+  await delay();
+  __options__[data.projectId] = __options__[data.projectId] || [];
+  __options__[data.projectId].push({ optionId: `o_${Date.now()}`, optionCode: data.optionCode, optionName: data.optionName, score: Number(data.score) || 0 });
+  return true as unknown as any;
+};
+
+// 删除选项
+export const delOption = async (optionId: string) => {
+  await delay();
+  Object.keys(__options__).forEach((k) => {
+    __options__[k] = (__options__[k] || []).filter((o) => o.optionId !== optionId);
+  });
+  return true as unknown as any;
+};
+
+// 更新选项分数
+export const updateOptionScore = async (optionId: string, score: number) => {
+  await delay();
+  Object.keys(__options__).forEach((k) => {
+    (__options__[k] || []).forEach((o) => {
+      if (o.optionId === optionId) o.score = Number(score) || 0;
+    });
+  });
+  return true as unknown as any;
+};
+
+

+ 166 - 0
src/pages/platform/setting/serviceEvaluate/style.less

@@ -0,0 +1,166 @@
+// 复用 systemNavMana 的布局风格
+.SystemNavMana {
+  display: flex;
+  flex-direction: row;
+  background: #F5F7FA;
+
+  .leftTree {
+    width: 220px;
+    height: calc(100vh - 80px);
+    background: #FFF;
+    border-radius: 4px;
+    overflow: hidden;
+    margin-right: 16px;
+    .search { padding: 16px; display:flex; align-items:center; gap:8px; }
+    .search .add {
+      cursor: pointer;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      width: 24px;
+      height: 24px;
+      background: #FAFCFF;
+      border-radius: 4px;
+      border: 1px solid #DAE2F2;
+      flex: 0 0 24px;
+      min-width: 24px;
+    }
+    .searchInput { border: 1px solid #CFD7E6; flex: 1; min-width: 0; }
+    .add { cursor: pointer; display: flex; justify-content: center; align-items: center; width: 24px; height: 24px; background: #FAFCFF; border-radius: 4px;border: 1px solid #DAE2F2;}
+    .treeeWrap { height: calc(100% - 50px); overflow-y: scroll; padding: 16px; padding-top: 0;padding-right: 8px; }
+    // 选中与悬浮样式还原
+    .kcmp-ant-tree.kcmp-ant-tree-directory .kcmp-ant-tree-treenode {
+      margin-bottom: 2px !important;
+      padding-bottom: 0;
+      &:hover { border-radius: 4px !important; background: #F0F2F5; }
+      &::before { bottom: 0 !important; }
+    }
+    // 悬浮到右侧新增图标时,不触发行背景高亮;但图标自身hover需要有背景
+    .kcmp-ant-tree-node-content-wrapper:hover .node-title-area + span:not(:hover) { background: transparent !important; }
+    .inline-add { cursor: pointer; width: 20px; height: 20px; border-radius: 4px; }
+    .inline-add:hover { background: #DAE2F2; }
+    .kcmp-ant-tree.kcmp-ant-tree-directory .kcmp-ant-tree-treenode-selected:hover::before,
+    .kcmp-ant-tree.kcmp-ant-tree-directory .kcmp-ant-tree-treenode-selected::before { border-radius: 4px; background: #F0F2F5; }
+    .kcmp-ant-tree.kcmp-ant-tree-directory .kcmp-ant-tree-treenode .kcmp-ant-tree-node-content-wrapper.kcmp-ant-tree-node-selected { font-weight: 500; color:#17181A; }
+    // 缩进与展开图标位置
+    .kcmp-ant-tree.kcmp-ant-tree-directory .kcmp-ant-tree-treenode .kcmp-ant-tree-indent .kcmp-ant-tree-indent-unit { width: 8px !important; }
+    .kcmp-ant-tree.kcmp-ant-tree-directory .kcmp-ant-tree-treenode .kcmp-ant-tree-switcher > span { top: 7px !important; }
+  }
+
+  .rightContent {
+    position: relative;
+    border-radius: 4px;
+    width: calc(100% - 220px);
+    padding: 16px;
+    background: #FFF;
+
+    .tableToolbar {
+      display: flex;
+      flex-direction: row;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 16px;
+      gap: 16px;
+      .filter { 
+        display: flex; 
+        flex-direction: row; 
+        align-items: center;
+        flex: 1;
+        min-width: 0;
+      }
+      .filter .filterItem { 
+        display: flex; 
+        flex-direction: row; 
+        align-items: center; 
+      }
+      
+      .btnGroup {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        flex-shrink: 0;
+        min-width: max-content;
+        
+        span {
+          box-sizing: border-box;
+          cursor: pointer;
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+          font-size: 14px;
+          font-family: SourceHanSansCN-Normal, SourceHanSansCN;
+          font-weight: 400;
+          color: #17181A;
+          border: 1px solid #DAE2F2;
+          height: 26px;
+          min-height: 26px;
+          padding: 0 14px;
+          background: #FAFCFF;
+          border-radius: 4px;
+          white-space: nowrap;
+          flex-shrink: 0;
+          width: auto !important;
+          max-width: none !important;
+        }
+
+        .collapse {
+          margin-right: 8px;
+        }
+
+        .add {
+          color: #FFFFFF;
+          background: #3377FF;
+          border: 1px solid #3377FF;
+        }
+      }
+    }
+  }
+}
+
+.KC-DirectoryTree .kcmp-ant-tree-list { height: 100%; }
+
+// 选项子表样式
+.SE-SubTitle { font-weight: 600; }
+.SE-OptionsWrap { padding: 12px 8px; background: #fafbfd; border: 1px solid #dae2f2; border-radius: 4px; }
+.SE-OptionsHeader { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
+.SE-OptionsTable { width: 100%; }
+.SE-OptionsRow { display: grid; grid-template-columns: 60px 140px 1fr 160px 100px; align-items: center; min-height: 40px; border-top: 1px solid #eff2f7; }
+.SE-OptionsHead { background: #f7f9fc; border-top: none; }
+.SE-OptionsRow .col { padding: 6px 8px; }
+.SE-Empty { padding: 8px; color: #a3adbf; }
+
+// 展开区内嵌表格容器,避免覆盖外层行
+.SE-ExpandWrap {
+  padding: 8px 8px 8px 12px;
+  margin-left: 48px; // 缩进,便于层次识别
+  background: #FAFBFD;
+  border-left: 1px solid #DAE2F2; // 仅保留左侧引导线
+  border-top: 0;
+  border-right: 0;
+  border-bottom: 0;
+  border-radius: 0;
+  overflow: hidden; // 防止内表格边框/阴影越界覆盖外层
+  user-select: none; // 避免拖拽时选中文本影响体验
+
+  // 覆盖内嵌 KCTable 的外边框,避免双层边框
+  .KCTable {
+    border: 0 !important;
+  }
+}
+
+// 添加服务 - Transfer 样式修正
+.ServiceTransfer {
+  width: 100%;
+  display: flex;
+  .kcmp-ant-transfer-list {
+    flex: 1;
+    min-width: 0;
+  }
+  .kcmp-ant-transfer-operation {
+    display: flex;
+    align-items: center;
+    margin: 0 12px;
+  }
+}
+
+

+ 18 - 5
src/pages/platform/setting/systemNavMana/index.tsx

@@ -50,7 +50,11 @@ const MonthlyInfoCheck: React.FC = () => {
 
   const [reload, set_reload] = useState(false);
 
-  const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>();
+  const [tableDataFilterParams, set_tableDataFilterParams] = useState<TableRequestParamsType>({
+    current: 1,
+    pageSize: 10,
+    name: ''
+  });
 
   const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
   const [searchValue, setSearchValue] = useState('');
@@ -412,9 +416,17 @@ const MonthlyInfoCheck: React.FC = () => {
     set_tableDataFilterParams({
       ...tableDataFilterParams,
       current: 1,
-      [`${paramName}`]: tableDataSearchKeywords,
+      [`${paramName}`]: tableDataSearchKeywords
     });
-  };
+  }
+
+  const handleTableChange = (pagination: any) => {
+    set_tableDataFilterParams({
+      ...tableDataFilterParams,
+      current: pagination.current,
+      pageSize: pagination.pageSize
+    });
+  }
 
   const updateTable = async (type: 'EDIT' | 'ADD', formVal: any, record: any) => {
     if (type == 'ADD') {
@@ -702,7 +714,7 @@ const MonthlyInfoCheck: React.FC = () => {
             <div className="filterItem">
               <span className="label">检索:</span>
               <KCInput
-                placeholder={'请输入菜单名称'}
+                placeholder={'请输入名称'}
                 style={{ width: 160 }}
                 search
                 allowClear
@@ -712,7 +724,7 @@ const MonthlyInfoCheck: React.FC = () => {
                     set_tableDataFilterParams({
                       ...tableDataFilterParams,
                       current: 1,
-                      name: '',
+                      name: ''
                     });
                   }
                 }}
@@ -735,6 +747,7 @@ const MonthlyInfoCheck: React.FC = () => {
             rowKey="menuId"
             columns={columns as ProColumns[]}
             request={(params: any, sort: any, filter: any) => getTableData(params, sort, filter)}
+            onChange={handleTableChange}
           />
         )}
       </div>

+ 32 - 51
src/pages/platform/setting/userManage/index.tsx

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2022-01-11 09:43:18
- * @LastEditTime: 2025-01-14 10:50:18
+ * @LastEditTime: 2025-05-13 15:32:58
  * @LastEditors: code4eat awesomedema@gmail.com
  * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  * @FilePath: /KC-MiddlePlatform/src/pages/platform/userManage/index.tsx
@@ -39,7 +39,13 @@ interface PageProps extends ConnectProps {
 const UserManage: FC<PageProps> = ({ userManageModel: state, dispatch }) => {
   const { reloadTable } = state;
   const { initialState, setInitialState } = useModel('@@initialState');
-  const [tableDataFilterParams, set_tableDataFilterParams] = useState<any | undefined>();
+  const [tableDataFilterParams, set_tableDataFilterParams] = useState<TableRequestParamsType>({
+    current: 1,
+    pageSize: 10,
+    name: '',
+    account: '',
+    departmentId: undefined
+  });
   const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
 
   const [dirData, set_dirData] = useState<UserRelaSeletDataType[]>([]);
@@ -245,9 +251,17 @@ const UserManage: FC<PageProps> = ({ userManageModel: state, dispatch }) => {
     set_tableDataFilterParams({
       ...tableDataFilterParams,
       current: 1,
-      [`${paramName}`]: tableDataSearchKeywords,
+      [`${paramName}`]: tableDataSearchKeywords
     });
-  };
+  }
+
+  const handleTableChange = (pagination: any) => {
+    set_tableDataFilterParams({
+      ...tableDataFilterParams,
+      current: pagination.current,
+      pageSize: pagination.pageSize
+    });
+  }
 
   const importData = (name: string) => {
     return (
@@ -261,7 +275,7 @@ const UserManage: FC<PageProps> = ({ userManageModel: state, dispatch }) => {
           } = values;
 
           let formData = new FormData();
-          console.log({ values });
+
           formData.append('file', fileList[0].originFileObj);
 
           dispatch &&
@@ -358,6 +372,7 @@ const UserManage: FC<PageProps> = ({ userManageModel: state, dispatch }) => {
                     ...tableDataFilterParams,
                     current: 1,
                     name: '',
+                    account: ''
                   });
                 }
               }}
@@ -373,52 +388,18 @@ const UserManage: FC<PageProps> = ({ userManageModel: state, dispatch }) => {
         </div>
       </div>
 
-      <KCTable
-        rowKey="id"
-        headerTitle="查询表格"
-        columns={columns}
-        reload={reloadTable}
-        params={tableDataFilterParams}
-        newVer
-        scroll={{ y: `calc(100vh - 255px)` }}
-        expandable={{
-          expandedRowRender: (record) => (
-            <div className="userExpandInfo">
-              {initialState?.customerType == '2' ? <span>入职时间:{record.entryTime}</span> : <span>入职时间:{record.entryTime}</span>}
-              {initialState?.customerType == '2' ? <span>联系电话:{record.phoneNumber}</span> : <span>联系电话:{record.phoneNumber}</span>}
-              <span>所学专业:{record.major}</span>
-              <span>资格证号:{record.qualificationCertificateNo}</span>
-              {initialState?.customerType == '2' ? <span>执业级别:{record.doctorLevel}</span> : <span>医师级别:{record.doctorLevel}</span>}
-              <span>执业证号:{record.practiceCertificateNo}</span>
-              <span>执业类别:{record.practiceSubject}</span>
-              <span>执业科目:{record.practiceCate}</span>
-              <span>执业状态:{record.practiceStatus}</span>
-              <span>备注:{record.remark}</span>
-            </div>
-          ),
-          rowExpandable: () => true,
-        }}
-        // toolBarRender={() => [
-        //   <KCUpload
-        //     title="导入数据"
-        //     fieldProps={{
-        //       onChange: ({ file, fileList }:{file:any,fileList:any}) => {
-        //         if (file.status == 'done') {
-        //           importUserHandle(file.originFileObj);
-        //         }
-        //       },
-        //       showUploadList: false,
-        //     }}
-        //   />,
-        //   <Button key="2" onClick={() => getUserTemplateHandle()}>
-        //     模板下载
-        //   </Button>,
-        //   <Button key="3" onClick={addUserHandle}>
-        //     新增用户
-        //   </Button>,
-        // ]}
-        request={(params) => getUserData(params)}
-      />
+      <div style={{ marginTop: 16 }}>
+        <KCTable
+          columns={columns}
+          scroll={{ y: `calc(100vh - 250px)` }}
+          reload={reloadTable}
+          rowKey="id"
+          newVer
+          params={tableDataFilterParams}
+          request={(params) => getUserData(params)}
+          onChange={handleTableChange}
+        />
+      </div>
     </div>
   );
 };

+ 11 - 1
src/service/indicator.ts

@@ -2,7 +2,7 @@
  * @Author: code4eat awesomedema@gmail.com
  * @Date: 2022-07-15 10:13:10
  * @LastEditors: code4eat awesomedema@gmail.com
- * @LastEditTime: 2025-03-07 13:42:09
+ * @LastEditTime: 2025-04-25 10:07:12
  * @FilePath: /KC-MiddlePlatform/src/service/indicator.ts
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */
@@ -99,3 +99,13 @@ export const getIndicatorCateList_old = async (params?: { menuCode: string }) =>
     params: params,
   });
 };
+
+
+//编辑指标绑定的报告
+
+export const applyOnlineReportMap = async (params?: { code: string,onlineReportCode:string,onlineRemark:string }) => {
+  return request<any[]>('/centerSys/indicator/applyOnlineReportMap', {
+    method: 'POST',
+    data: params,
+  });
+};

+ 168 - 92
src/utils.ts

@@ -1,22 +1,26 @@
 /*
  * @Author: your name
  * @Date: 2022-01-13 17:09:05
- * @LastEditTime: 2023-11-09 15:38:06
+ * @LastEditTime: 2025-04-17 11:01:43
  * @LastEditors: code4eat awesomedema@gmail.com
  * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  * @FilePath: /KC-MiddlePlatform/src/utils.js
  */
 
-import axios from 'axios';
+import axios, { AxiosResponse } from 'axios';
 import { Key } from 'react';
 
-export const randomString = (length: number) => {
+/**
+ * 生成指定长度的随机字符串
+ * @param length 字符串长度
+ * @returns 随机字符串
+ */
+export const randomString = (length: number): string => {
   const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_=-';
-  let result = '';
-  for (let i = length; i > 0; --i) {
-    result += chars[Math.floor(Math.random() * chars.length)];
-  }
-  return result;
+  return Array.from(
+    { length },
+    () => chars[Math.floor(Math.random() * chars.length)]
+  ).join('');
 };
 
 export interface TreeItemType {
@@ -24,66 +28,100 @@ export interface TreeItemType {
   [key: string]: any;
 }
 
-//获取树结构指定的值集合
-export const getValsFromTree = (data: TreeItemType[], key: string) => {
-  let result: any[] = [];
-  function looper(data: TreeItemType[], key: string) {
-    data.forEach((t) => {
-      if (t[key]) {
-        result.push(t[key]);
+/**
+ * 从树结构中获取指定键的所有值
+ * @param data 树结构数据
+ * @param key 要获取的键名
+ * @returns 值的数组
+ */
+export const getValsFromTree = (data: TreeItemType[], key: string): any[] => {
+  const result: any[] = [];
+  
+  const traverse = (nodes: TreeItemType[]): void => {
+    nodes.forEach((node) => {
+      if (node[key]) {
+        result.push(node[key]);
       }
-      if (t.children && t.children.length > 0) {
-        looper(t.children, key);
+      if (node.children?.length) {
+        traverse(node.children);
       }
     });
-  }
-  looper(data, key);
+  };
+
+  traverse(data);
   return result;
 };
 
 export const searchTree = (treeData: TreeItemType[]) => {};
 
-export const downloadTemplateReq = (path: string) => {
-  const userData = localStorage.getItem('userData');
-  const { token = '' } = JSON.parse(userData as any);
-
-  axios({
-    method: 'get',
-    url: `/gateway/${path}`,
-    responseType: 'blob',
-    headers: { token },
-  }).then(function (response: any) {
-    //console.log({ 'chunk': response });
+/**
+ * 下载模板文件
+ * @param path 文件路径
+ */
+export const downloadTemplateReq = async (path: string): Promise<void> => {
+  try {
+    const userData = localStorage.getItem('userData');
+    const { token = '' } = JSON.parse(userData || '{}');
+
+    const response: AxiosResponse = await axios({
+      method: 'get',
+      url: `/gateway/${path}`,
+      responseType: 'blob',
+      headers: { token },
+    });
+
     const filename = decodeURI(response.headers['content-disposition']);
-    const objectUrl = URL.createObjectURL(
-      new Blob([response.data], {
-        type: 'application/vnd.ms-excel',
-      }),
-    );
+    const blob = new Blob([response.data], {
+      type: 'application/vnd.ms-excel',
+    });
+    const objectUrl = URL.createObjectURL(blob);
+    
     const link = document.createElement('a');
-    // 设置导出的文件名称
-    link.download = `${filename}` + '.xls';
-    link.style.display = 'none';
+    link.download = `${filename}.xls`;
     link.href = objectUrl;
-    link.click();
+    link.style.display = 'none';
     document.body.appendChild(link);
-  });
+    link.click();
+    document.body.removeChild(link);
+    URL.revokeObjectURL(objectUrl);
+  } catch (error) {
+    console.error('下载模板失败:', error);
+    throw error;
+  }
 };
 
-let parent: any = undefined;
-
-export const getDeepestTreeData: any = (tree: any, childrenName: string) => {
-  if (tree[`${childrenName}`] && tree[`${childrenName}`].length > 0) {
-    parent = tree;
-    return getDeepestTreeData(tree[`${childrenName}`][0], childrenName);
-  }
+/**
+ * 获取树结构中最深的节点及其父节点
+ * @param tree 树结构
+ * @param childrenName 子节点属性名
+ * @returns [最深节点, 父节点]
+ */
+export const getDeepestTreeData = <T extends { [key: string]: any }>(
+  tree: T,
+  childrenName: string
+): [T, T | undefined] => {
+  let parent: T | undefined;
+  
+  const traverse = (node: T): T => {
+    if (node[childrenName]?.length) {
+      parent = node;
+      return traverse(node[childrenName][0]);
+    }
+    return node;
+  };
 
-  return [tree, parent];
+  return [traverse(tree), parent];
 };
 
-export const uniqueFunc = (arr: any[], uniId: string) => {
-  const res = new Map();
-  return arr.filter((item) => !res.has(item[uniId]) && res.set(item[uniId], 1));
+/**
+ * 数组去重
+ * @param arr 数组
+ * @param uniId 唯一标识字段
+ * @returns 去重后的数组
+ */
+export const uniqueFunc = <T extends { [key: string]: any }>(arr: T[], uniId: string): T[] => {
+  const uniqueMap = new Map();
+  return arr.filter((item) => !uniqueMap.has(item[uniId]) && uniqueMap.set(item[uniId], 1));
 };
 
 //获取树结构的所有叶子节点
@@ -93,30 +131,45 @@ interface TreeNode {
   child?: TreeNode[];
 }
 
+/**
+ * 获取树的所有叶子节点
+ * @param node 树节点
+ * @param leaves 叶子节点数组
+ * @returns 叶子节点数组
+ */
 export const getLeafNodes = (node: TreeNode, leaves: TreeNode[] = []): TreeNode[] => {
-  if (!node.child || node.child.length === 0) {
+  if (!node.child?.length) {
     leaves.push(node);
   } else {
-    for (const child of node.child) {
-      getLeafNodes(child, leaves);
-    }
+    node.child.forEach((child) => getLeafNodes(child, leaves));
   }
   return leaves;
 };
 
 //根据树结构中的某个属性的集合,获取对应的节点,返回一个集合
 
-export const findNodesBySomes = (node: TreeNode, keys: Set<string | number>, str: string): TreeNode[] => {
-  let results: TreeNode[] = [];
+/**
+ * 根据属性值查找节点
+ * @param node 树节点
+ * @param keys 属性值集合
+ * @param str 属性名
+ * @returns 匹配的节点数组
+ */
+export const findNodesBySomes = (
+  node: TreeNode,
+  keys: Set<string | number>,
+  str: string
+): TreeNode[] => {
+  const results: TreeNode[] = [];
 
-  if (keys.has(node[`${str}`])) {
+  if (keys.has(node[str])) {
     results.push(node);
   }
 
-  if (node.child) {
-    for (let child of node.child) {
-      results = results.concat(findNodesBySomes(child, keys, str));
-    }
+  if (node.child?.length) {
+    node.child.forEach((child) => {
+      results.push(...findNodesBySomes(child, keys, str));
+    });
   }
 
   return results;
@@ -124,59 +177,82 @@ export const findNodesBySomes = (node: TreeNode, keys: Set<string | number>, str
 
 //更改树结构集合的子集属性名
 
-export const renameChildListToChildren = (nodes: any[], key: string) => {
+/**
+ * 重命名子节点属性
+ * @param nodes 节点数组
+ * @param key 要重命名的属性名
+ * @returns 处理后的节点数组
+ */
+export const renameChildListToChildren = <T extends { [key: string]: any; children?: T[] }>(
+  nodes: T[],
+  key: string
+): T[] => {
   return nodes.map((node) => {
-    // 创建当前节点的副本
     const newNode = { ...node };
-
-    // 如果当前节点有 childList,则重命名为 children,并递归处理子节点
-    if (newNode[`${key}`] && newNode[`${key}`].length > 0) {
-      newNode.children = renameChildListToChildren(newNode[`${key}`], key);
+    if (newNode[key]?.length) {
+      newNode.children = renameChildListToChildren(newNode[key], key);
     }
-
     return newNode;
   });
 };
 
 //获取目标节点的所有父节点,且以集合的方式返回
 
-export const findParents = (tree: TreeNode[], targetNodeKeyName: string, targetNodeKeyVal: Key, path: TreeNode[] = []): TreeNode[] | null => {
+/**
+ * 查找节点的所有父节点
+ * @param tree 树结构
+ * @param targetNodeKeyName 目标节点键名
+ * @param targetNodeKeyVal 目标节点键值
+ * @param path 路径
+ * @returns 父节点数组或null
+ */
+export const findParents = (
+  tree: TreeNode[],
+  targetNodeKeyName: string,
+  targetNodeKeyVal: Key,
+  path: TreeNode[] = []
+): TreeNode[] | null => {
   for (const node of tree) {
-    // 如果找到目标节点,返回包含所有父节点的路径
-    if (node[`${targetNodeKeyName}`] === targetNodeKeyVal) {
+    if (node[targetNodeKeyName] === targetNodeKeyVal) {
       return [...path, node];
     }
 
-    // 如果当前节点有子节点,递归搜索子节点
-    if (node.children && node.children.length > 0) {
+    if (node.children?.length) {
       const result = findParents(node.children, targetNodeKeyName, targetNodeKeyVal, [...path, node]);
-      if (result) {
-        return result;
-      }
+      if (result) return result;
     }
   }
-
-  // 如果在当前分支找不到目标节点,返回 null
   return null;
 };
 
 //搜索树结果
 
-export const searchTreeAndKeepStructure = (nodes: any[], keyName: string, keyVal: any) => {
-  let result: any[] = [];
-
-  nodes.forEach((node) => {
-    // 检查当前节点是否匹配
-    if (node[`${keyName}`].indexOf(keyVal) != -1) {
-      result.push(node.children && node.children.length ? { ...node, children: searchTreeAndKeepStructure(node.children, keyName, keyVal) } : { ...node });
-    } else if (node.children && node.children.length) {
-      // 检查子节点
-      let children = searchTreeAndKeepStructure(node.children, keyName, keyVal);
+/**
+ * 搜索树结构并保持结构
+ * @param nodes 节点数组
+ * @param keyName 键名
+ * @param keyVal 键值
+ * @returns 匹配的节点数组
+ */
+export const searchTreeAndKeepStructure = <T extends { [key: string]: any }>(
+  nodes: T[],
+  keyName: string,
+  keyVal: any
+): T[] => {
+  return nodes.reduce<T[]>((result, node) => {
+    if (String(node[keyName]).includes(String(keyVal))) {
+      result.push({
+        ...node,
+        children: node.children?.length
+          ? searchTreeAndKeepStructure(node.children, keyName, keyVal)
+          : undefined,
+      });
+    } else if (node.children?.length) {
+      const children = searchTreeAndKeepStructure(node.children, keyName, keyVal);
       if (children.length) {
         result.push({ ...node, children });
       }
     }
-  });
-
-  return result;
+    return result;
+  }, []);
 };

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff