Sfoglia il codice sorgente

feat: 更新项目优化相关功能

code4eat 1 mese fa
parent
commit
8b69d5f404
54 ha cambiato i file con 2997 aggiunte e 691 eliminazioni
  1. 15 5
      config/config.ts
  2. 97 3
      config/proxy.ts
  3. 47 45
      package.json
  4. BIN
      public/images/login_arrow_white.png
  5. BIN
      public/images/odds.png
  6. BIN
      public/images/odds_gray.png
  7. 1 1
      public/zhongtaiC.js
  8. 28 23
      src/app.tsx
  9. 155 0
      src/components/GlobalErrorBoundary/index.tsx
  10. 72 0
      src/components/GlobalErrorBoundary/style.less
  11. 4 0
      src/components/NavSelecter/index.tsx
  12. 1 1
      src/components/intelligenceBot/Chat/index.tsx
  13. 295 11
      src/components/topBar/index.tsx
  14. 230 0
      src/components/topBar/style.less
  15. 9 1
      src/global.less
  16. 2 2
      src/global.tsx
  17. 32 11
      src/layouts/index.tsx
  18. 41 17
      src/pages/index/components/FastEntry/index.tsx
  19. 7 0
      src/pages/index/components/FastEntry/style.less
  20. 12 13
      src/pages/index/components/TodoList/index.tsx
  21. 24 7
      src/pages/index/index.tsx
  22. 352 50
      src/pages/login/index.tsx
  23. 108 10
      src/pages/login/style.less
  24. 63 199
      src/pages/personalCenter/components/base.tsx
  25. 64 22
      src/pages/personalCenter/components/security.tsx
  26. 1 1
      src/pages/personalCenter/index.tsx
  27. 8 36
      src/pages/platform/_layout.tsx
  28. 191 0
      src/pages/platform/setting/aiPromptMana/index.tsx
  29. 55 0
      src/pages/platform/setting/aiPromptMana/service.ts
  30. 50 0
      src/pages/platform/setting/aiPromptMana/style.less
  31. 82 23
      src/pages/platform/setting/dataFilling/fillingMana/index.tsx
  32. 5 5
      src/pages/platform/setting/dataFilling/mineFilling/style.less
  33. 42 15
      src/pages/platform/setting/departmentMana/index.tsx
  34. 28 0
      src/pages/platform/setting/departmentMana/service.ts
  35. 87 7
      src/pages/platform/setting/embeddedDashboard/index.tsx
  36. 18 33
      src/pages/platform/setting/hospManage/index.tsx
  37. 1 1
      src/pages/platform/setting/indicatoLagacy/index.tsx
  38. 315 22
      src/pages/platform/setting/indicatorMana/index.tsx
  39. 12 0
      src/pages/platform/setting/indicatorMana/style.less
  40. 27 4
      src/pages/platform/setting/pubDicMana/index.tsx
  41. 34 9
      src/pages/platform/setting/pubDicTypeMana/index.tsx
  42. 2 0
      src/pages/platform/setting/pubDicTypeMana/service.ts
  43. 132 90
      src/pages/platform/setting/roleManage/index.tsx
  44. 3 2
      src/pages/platform/setting/serviceEvaluate/index.tsx
  45. 1 0
      src/pages/platform/setting/static/index.tsx
  46. 127 4
      src/pages/platform/setting/userManage/index.tsx
  47. 13 14
      src/pages/platform/setting/userManage/modal.tsx
  48. 11 0
      src/pages/platform/setting/userManage/style.less
  49. 1 1
      src/service/hospList.ts
  50. 47 1
      src/service/index.ts
  51. 13 1
      src/service/indicator.ts
  52. 23 1
      src/service/login.ts
  53. 1 0
      src/service/role.ts
  54. 8 0
      src/service/user.ts

+ 15 - 5
config/config.ts

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2022-01-07 10:04:20
- * @LastEditTime: 2025-04-08 11:45:42
+ * @LastEditTime: 2025-10-30 14:57: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
@@ -197,10 +197,10 @@ export default defineConfig({
                   component: '@/pages/platform/setting/roleManage/index.tsx',
                   wrappers: ['@/wrappers/auth'],
                 },
-                // {
-                //   path: '/platform/setting/reports',
-                //   component: '@/pages/platform/setting/reports/index.tsx',
-                // },
+                {
+                  path: '/platform/setting/reports',
+                  component: '@/pages/platform/setting/reports/index.tsx',
+                },
                 {
                   path: '/platform/setting/departmentMana',
                   component: '@/pages/platform/setting/departmentMana/index.tsx',
@@ -246,11 +246,21 @@ export default defineConfig({
                   component: '@/pages/platform/setting/notificationTemplate/index.tsx',
                   wrappers: ['@/wrappers/auth'],
                 },
+                {
+                  path: '/platform/setting/aiPromptMana',
+                  component: '@/pages/platform/setting/aiPromptMana/index.tsx',
+                  wrappers: ['@/wrappers/auth'],
+                },
                 {
                   path: '/platform/setting/kcClassification',
                   component: '@/pages/platform/setting/kcClassification/index.tsx',
                   wrappers: ['@/wrappers/auth'],
                 },
+                {
+                  path: '/platform/setting/serviceEvaluate',
+                  component: '@/pages/platform/setting/serviceEvaluate/index.tsx',
+                  wrappers: ['@/wrappers/auth'],
+                },
                 {
                   path: '/platform/setting/dataFilling',
                   name: '数据填报',

+ 97 - 3
config/proxy.ts

@@ -2,23 +2,103 @@
  * @Author: code4eat awesomedema@gmail.com
  * @Date: 2024-04-09 18:07:34
  * @LastEditors: code4eat awesomedema@gmail.com
- * @LastEditTime: 2025-05-21 17:30:57
+ * @LastEditTime: 2026-04-17 15:39:05
  * @FilePath: /KC-MiddlePlatform/config/proxy.ts
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */
 
 const proxy: { [key: string]: any } = {
   dev: {
+    // WebSocket 专用代理,避免被 /gateway 先匹配成 HTTP
+    '/gateway/centerSys/websocket': {
+      // 本地开发按实际网关地址转发
+      target: 'ws://dev.kcim.cn',
+      changeOrigin: true,
+      ws: true,
+      // 后端实际路径不带 /gateway 前缀
+      // pathRewrite: { '^/gateway': '' },
+      // 将 query 中的 token 写入 Header,后端只认 Header
+      onProxyReq(proxyReq: any, req: any) {
+        proxyReq.on('error', (err: any) => {
+          console.error('WS proxyReq error:', err?.message || err);
+        });
+        try {
+          const url = new URL(req.url, 'http://placeholder');
+          const token = url.searchParams.get('token');
+          if (token) {
+            proxyReq.setHeader('token', token);
+          }
+        } catch (e) {
+          // ignore
+        }
+      },
+      onProxyReqWs(proxyReq: any, req: any) {
+        proxyReq.on('error', (err: any) => {
+          console.error('WS proxyReqWs error:', err?.message || err);
+        });
+        try {
+          const url = new URL(req.url, 'http://placeholder');
+          const token = url.searchParams.get('token');
+          if (token) {
+            proxyReq.setHeader('token', token);
+          }
+        } catch (e) {
+          // ignore
+        }
+      },
+      onError(err: any, req: any, res: any) {
+        console.error('WS proxy error:', err?.message || err);
+        try {
+          res.writeHead(502);
+          res.end('WS proxy error');
+        } catch (_) { }
+      },
+    },
     '/gateway': {
-      //target: 'http://47.96.149.190:5000',
-      target: 'http://120.27.235.181:5000',
+      //target: 'http://47.96.149.190',
+      target: 'http://120.27.235.181',
       //target: 'http://platform.pre.bs.qjczt.com:5000',
       changeOrigin: true,
+      timeout: 30000,
+      proxyTimeout: 30000,
+      onProxyReq(proxyReq: any) {
+        proxyReq.on('error', (err: any) => {
+          console.error('Gateway proxyReq error:', err?.message || err);
+        });
+      },
+      onError(err: any, req: any, res: any) {
+        console.error('Gateway proxy error:', err?.message || err);
+        try {
+          res.writeHead(502);
+          res.end('Gateway proxy error');
+        } catch (_) { }
+      },
       // pathRewrite: { '^/master': '' },
     },
     '/api': {
       target: 'http://120.27.235.181:8088',
       changeOrigin: true,
+      timeout: 30000,
+      proxyTimeout: 30000,
+      onProxyReq(proxyReq: any) {
+        proxyReq.on('error', (err: any) => {
+          console.error('API proxyReq error:', err?.message || err);
+        });
+      },
+      onError(err: any, req: any, res: any) {
+        console.error('API proxy error:', err?.message || err);
+        try {
+          res.writeHead(502);
+          res.end('API proxy error');
+        } catch (_) { }
+      },
+      // 本地开发:对 /api/se 开头的请求不走代理,交由 umi mock 处理
+      bypass: function (req: any) {
+        if (req && req.url && req.url.startsWith('/api/se')) {
+          return req.url;
+        }
+        return undefined;
+      },
     },
     '/ai': {
       target: 'http://192.168.0.120:5003',
@@ -33,6 +113,20 @@ const proxy: { [key: string]: any } = {
         proxyRes.on('end', function () {
           res.end();
         });
+        proxyRes.on('error', function (err: any) {
+          console.error('AI ProxyRes error:', err);
+          res.end();
+        });
+        res.on('error', function (err: any) {
+          console.error('AI Res error:', err);
+        });
+      },
+      onError(err: any, req: any, res: any) {
+        console.error('AI proxy error:', err?.message || err);
+        try {
+          res.writeHead(502);
+          res.end('AI proxy error');
+        } catch (_) { }
       },
     },
   },

+ 47 - 45
package.json

@@ -1,57 +1,59 @@
 {
     "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"
+        "start": "cross-env NODE_OPTIONS=--openssl-legacy-provider umi dev",
+        "start:dev": "cross-env NODE_OPTIONS=--openssl-legacy-provider REACT_APP_ENV=dev UMI_ENV=dev umi dev",
+        "build": "cross-env NODE_OPTIONS=--openssl-legacy-provider 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"
+        "pre-commit": "lint-staged"
     },
     "lint-staged": {
-      "*.{js,jsx,less,md,json}": [
-        "prettier --write"
-      ],
-      "*.ts?(x)": [
-        "prettier --parser=typescript --write"
-      ]
+        "*.{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"
+        "@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-dnd": "^16.0.1",
+        "react-dnd-html5-backend": "^16.0.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"
+        "@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"
     }
-  }
+}

BIN
public/images/login_arrow_white.png


BIN
public/images/odds.png


BIN
public/images/odds_gray.png


+ 1 - 1
public/zhongtaiC.js

@@ -2,7 +2,7 @@
  * @Author: code4eat awesomedema@gmail.com
  * @Date: 2024-08-30 15:34:20
  * @LastEditors: code4eat awesomedema@gmail.com
- * @LastEditTime: 2025-04-24 10:47:56
+ * @LastEditTime: 2025-06-17 15:32:53
  * @FilePath: /KC-MiddlePlatform/public/zhongtaiC.js
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */

+ 28 - 23
src/app.tsx

@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
 import { PageLoading } from '@ant-design/pro-layout';
 import { notification, Modal, message } from 'antd';
 import { RequestConfig, history } from 'umi';
@@ -34,7 +34,7 @@ const hospSign = getHospSign();
 
 const IconFont = createFromIconfontCN({
   scriptUrl: '/zhongtaiC.js',
-}); 
+});
 
 /** 获取用户信息比较慢的时候会展示一个 loading */
 export const initialStateConfig = {
@@ -95,12 +95,7 @@ const fetchUserInfo = async (): Promise<UserDataType | undefined> => {
     throw new Error('No user data found');
   } catch (error) {
     const currentUrlParams = new URLSearchParams(window.location.search);
-    const defaultHospSign = 'yourDefaultHospSign';
-
-    if (!currentUrlParams.has('hospSign')) {
-      currentUrlParams.append('hospSign', defaultHospSign);
-    }
-
+    // 不再自动补齐 hospSign,仅跳转到登录页并保留现有查询参数
     history.push(`${loginPath}?${currentUrlParams.toString()}`);
     return undefined;
   }
@@ -110,7 +105,7 @@ const fetchUserInfo = async (): Promise<UserDataType | undefined> => {
  * 获取医院子系统列表
  */
 const getHospSubSystemListFunc = async (): Promise<any[]> => {
-  // TODO: 实现获取子系统列表的逻辑
+  // TODO: 实现获取子系统列表的逻辑 
   return [];
 };
 
@@ -188,6 +183,7 @@ const requestInterceptorsHandle = (url: string, options: MyRequestOptions) => {
 const responseInterceptorsHandle = async (response: Response, options: RequestOptionsInit) => {
   const { url, method } = options;
   const _response = await response.clone().json();
+  const skipSuccessMessage = Boolean((options as any)?.skipSuccessMessage);
 
   if (_response.errorCode === 401) {
     Modal.confirm({
@@ -204,7 +200,12 @@ const responseInterceptorsHandle = async (response: Response, options: RequestOp
   }
 
   if (_response.status === 200) {
-    if (url !== '/centerSys/menu/checkKeygen' && method === 'POST') {
+    if (
+      !skipSuccessMessage &&
+      url !== '/centerSys/menu/checkKeygen' &&
+      url != '/centerSys/user/checkPassword' &&
+      method === 'POST'
+    ) {
       message.success({
         content: '操作成功!',
         duration: 1,
@@ -358,30 +359,30 @@ export const qiankun = async () => {
         {
           name: 'budgetManaSystem', // 唯一 id
           //entry: '//localhost:8002',
-          entry: '//120.27.235.181:5000/perform/',  //开发
+          entry: '//120.27.235.181/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/', // 开发
+          //entry: '//localhost:8002'
+          entry: '//120.27.235.181/pfmManager/', // 开发
         },
         {
           name: 'CostAccountingSys', // 唯一 id
-          entry: '//localhost:8001',
-          //entry: '//120.27.235.181:5000/costAccount/', // 开发
+          entry: '//localhost:8004',
+          //entry: '//120.27.235.181/costAccount/', // 开发
         },
         {
           name: 'MediResourceManaSys', // 唯一 id
-          //entry: '//localhost:8001',
-          entry: '//120.27.235.181:5000/costAccount/', // 开发
+          //entry: '//localhost:8804',
+          entry: '//120.27.235.181/costAccount/', // 开发
+        },
+        {
+          name: 'MedicalWisdomCheckSys', // 唯一 id
+          //entry: '//localhost:8804',
+          entry: '//120.27.235.181:5000/pfmview/', // 开发
         },
-        // {
-        //   name: 'MedicalWisdomCheckSys', // 唯一 id
-        //   entry: '//localhost:8804',
-        //   //entry: '//120.27.235.181:5000/pfmview/', // 开发
-        // },
         // {
         //   name: 'personnelManaSystem', // 唯一 id
         //   entry: '//192.168.0.118:8005'
@@ -482,7 +483,11 @@ export const layout = ({ initialState }: { initialState?: InitialStateType }): B
     rightContentRender: () => <>right</>,
     footerRender: () => null,
     onPageChange: () => {
-      // 页面变化时的逻辑
+      // 根路径无页面组件,统一跳转到 /index
+      const { pathname } = history.location;
+      if (pathname === '/') {
+        history.replace('/index');
+      }
     },
     menuHeaderRender: undefined,
   };

+ 155 - 0
src/components/GlobalErrorBoundary/index.tsx

@@ -0,0 +1,155 @@
+/*
+ * @Author: code4eat awesomedema@gmail.com
+ * @Date: 2024-12-11 11:07:41
+ * @LastEditors: code4eat awesomedema@gmail.com
+ * @LastEditTime: 2024-12-11 11:08:26
+ * @FilePath: /KC-MiddlePlatform/src/components/GlobalErrorBoundary/index.tsx
+ * @Description: 全局错误边界组件,提供更友好的错误处理界面
+ */
+import React from 'react';
+import { Result, Button } from 'antd';
+import './style.less';
+
+interface ErrorBoundaryState {
+  hasError: boolean;
+  error: Error | null;
+  errorInfo: React.ErrorInfo | null;
+}
+
+interface Props {
+  children: React.ReactNode;
+  fallback?: React.ComponentType<{ error: Error; onRetry: () => void }>;
+}
+
+export class GlobalErrorBoundary extends React.Component<Props, ErrorBoundaryState> {
+  constructor(props: Props) {
+    super(props);
+    this.state = { hasError: false, error: null, errorInfo: null };
+  }
+
+  static getDerivedStateFromError(error: Error) {
+    // 更新 state,使下一次渲染能够显示降级后的 UI
+    return { hasError: true, error: error };
+  }
+
+  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
+    // 错误信息上报
+    console.error('GlobalErrorBoundary captured an error:', error, errorInfo);
+    
+    // 这里可以添加错误上报逻辑
+    this.reportError(error, errorInfo);
+    
+    this.setState({
+      error,
+      errorInfo
+    });
+  }
+
+  // 错误上报方法
+  reportError = (error: Error, errorInfo: React.ErrorInfo) => {
+    try {
+      // 可以在这里添加错误上报到监控系统的逻辑
+      const errorReport = {
+        message: error.message,
+        stack: error.stack,
+        componentStack: errorInfo.componentStack,
+        timestamp: new Date().toISOString(),
+        userAgent: navigator.userAgent,
+        url: window.location.href
+      };
+      
+      // 示例:发送到后端错误收集接口
+      // fetch('/api/error-report', {
+      //   method: 'POST',
+      //   headers: { 'Content-Type': 'application/json' },
+      //   body: JSON.stringify(errorReport)
+      // });
+      
+      console.log('Error Report:', errorReport);
+    } catch (reportError) {
+      console.error('Failed to report error:', reportError);
+    }
+  };
+
+  // 重置错误状态
+  resetError = () => {
+    this.setState({
+      hasError: false,
+      error: null,
+      errorInfo: null
+    });
+  };
+
+  // 刷新页面
+  refreshPage = () => {
+    window.location.reload();
+  };
+
+  render() {
+    if (this.state.hasError) {
+      // 如果提供了自定义的 fallback 组件,则使用它
+      if (this.props.fallback) {
+        const FallbackComponent = this.props.fallback;
+        return <FallbackComponent error={this.state.error!} onRetry={this.resetError} />;
+      }
+
+      // 默认的错误展示界面
+      return (
+        <div className="global-error-boundary">
+          <Result
+            status="error"
+            title="页面出现异常"
+            subTitle="抱歉,页面加载时发生了意外错误。请尝试刷新页面或联系管理员。"
+            extra={[
+              <Button type="primary" key="retry" onClick={this.resetError}>
+                重试
+              </Button>,
+              <Button key="refresh" onClick={this.refreshPage}>
+                刷新页面
+              </Button>,
+            ]}
+          >
+            {/* 在开发环境下显示详细错误信息 */}
+            {process.env.NODE_ENV === 'development' && this.state.error && (
+              <div className="error-details">
+                <h4>错误详情(仅开发环境显示):</h4>
+                <pre>{this.state.error.toString()}</pre>
+                {this.state.errorInfo && (
+                  <details style={{ whiteSpace: 'pre-wrap' }}>
+                    <summary>组件调用栈</summary>
+                    {this.state.errorInfo.componentStack}
+                  </details>
+                )}
+              </div>
+            )}
+          </Result>
+        </div>
+      );
+    }
+
+    return this.props.children;
+  }
+}
+
+// 自定义错误组件
+export const CustomErrorFallback: React.FC<{ error: Error; onRetry: () => void }> = ({ 
+  error, 
+  onRetry 
+}) => {
+  return (
+    <div className="custom-error-fallback">
+      <Result
+        status="warning"
+        title="组件加载失败"
+        subTitle={`错误信息:${error.message}`}
+        extra={[
+          <Button type="primary" key="retry" onClick={onRetry}>
+            重新加载
+          </Button>,
+        ]}
+      />
+    </div>
+  );
+};
+
+export default GlobalErrorBoundary;

+ 72 - 0
src/components/GlobalErrorBoundary/style.less

@@ -0,0 +1,72 @@
+/*
+ * 全局错误边界样式文件
+ */
+
+.global-error-boundary {
+  width: 100%;
+  height: 100vh;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #f5f5f5;
+
+  .ant-result {
+    background: white;
+    border-radius: 8px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+    padding: 40px;
+    max-width: 600px;
+    margin: 20px;
+  }
+
+  .error-details {
+    margin-top: 24px;
+    padding: 16px;
+    background: #f6f6f6;
+    border-radius: 4px;
+    border-left: 4px solid #ff4d4f;
+    
+    h4 {
+      color: #ff4d4f;
+      margin-bottom: 12px;
+    }
+    
+    pre {
+      background: #fff;
+      padding: 12px;
+      border-radius: 4px;
+      overflow-x: auto;
+      white-space: pre-wrap;
+      word-break: break-word;
+      color: #666;
+      font-size: 12px;
+      line-height: 1.4;
+    }
+    
+    details {
+      margin-top: 12px;
+      
+      summary {
+        cursor: pointer;
+        color: #1890ff;
+        outline: none;
+        
+        &:hover {
+          color: #40a9ff;
+        }
+      }
+    }
+  }
+}
+
+.custom-error-fallback {
+  width: 100%;
+  min-height: 400px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  
+  .ant-result {
+    padding: 24px;
+  }
+}

+ 4 - 0
src/components/NavSelecter/index.tsx

@@ -15,6 +15,7 @@ export type NavSelecterItemType = {
   type: number;
   contentType: number;
   systemId: string;
+  url?: string;
 };
 
 export type NavSelecterData = {
@@ -24,6 +25,7 @@ export type NavSelecterData = {
   systemId: string;
   type: number;
   contentType: number;
+  url?: string;
   child?: NavSelecterData[];
 };
 
@@ -74,6 +76,7 @@ const NavSelecter: React.FC<NavSelecterProps> = ({ onVisibleChange, data, value
               type: node.type,
               contentType: node.contentType,
               systemId: node.systemId || `${node.menuId}`,
+              url: node.url,
             });
           });
         }
@@ -135,6 +138,7 @@ const NavSelecter: React.FC<NavSelecterProps> = ({ onVisibleChange, data, value
               type: node.type,
               contentType: node.contentType,
               systemId: node.systemId || `${node.menuId}`,
+              url: node.url,
             });
           });
         }

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

@@ -171,7 +171,7 @@ const Chat: React.FC<ChatProps> = ({ messages, chatContainerRef, setMessages, on
     textArea.style.background = 'transparent';
     document.body.appendChild(textArea);
     textArea.select();
-    try {
+    try { 
       const successful = document.execCommand('copy');
       if (successful) setIfCopied(true);
     } catch (err) {

+ 295 - 11
src/components/topBar/index.tsx

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2021-11-16 09:12:37
- * @LastEditTime: 2025-01-20 10:38:30
+ * @LastEditTime: 2025-06-13 15:31:59
  * @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/components/topBar/index.tsx
@@ -10,7 +10,7 @@
 import React, { useEffect, useRef, useState } from 'react';
 
 import './style.less';
-import { Input, Modal, Tooltip } from 'antd';
+import { Badge, Input, Tooltip, Tabs, Spin, Empty, Modal, message } from 'antd';
 import { LogoutOutlined, SettingOutlined } from '@ant-design/icons';
 
 // import logo from '../../../public/images/kc-logo.png';
@@ -20,7 +20,8 @@ import { history, useLocation, useModel } from 'umi';
 import { Divider } from 'antd';
 import { updateTokenReq } from '@/service/user';
 import { logoutHandle } from '@/global';
-import { switchOrgReq } from '@/service';
+import { switchOrgReq, getMessageList, batchMessage } from '@/service';
+import { KcimCenterSysId } from '@/constant';
 
 interface TopBarType {
   onTabChange?: (data: TopBar.Tab[]) => void; //当tab切换时回调
@@ -29,7 +30,7 @@ interface TopBarType {
   userPannelTabClick?: (tag: 'SETTING' | 'LOGOUT' | 'SETUSERINFO') => void;
   onCloseTab?: (data: TopBar.Tab) => void;
   onTabClick?: (data: TopBar.Tab) => void;
-  userData?: { name: string; [key: string]: any };
+  userData?: { name: string;[key: string]: any };
   navData: TopBar.PanelData[];
   logo?: string;
   topBarTitle?: string;
@@ -59,6 +60,15 @@ const TopBar: React.FC<TopBarType> = (props) => {
   const [showGroupList, set_showGroupList] = useState(false);
   const [groupList, set_groupList] = useState<any[]>([]);
   const [currentActivedGroup, set_currentActivedGroup] = useState<any>(undefined);
+  const [unreadCount, setUnreadCount] = useState<number>(0);
+  const [notificationModalVisible, setNotificationModalVisible] = useState(false);
+  const [notificationTab, setNotificationTab] = useState<'unread' | 'read'>('unread');
+  const [notificationLoading, setNotificationLoading] = useState(false);
+  const [notificationBatchLoading, setNotificationBatchLoading] = useState(false);
+  const [notificationList, setNotificationList] = useState<{ unread: any[]; read: any[] }>({
+    unread: [],
+    read: [],
+  });
 
   const localSavedTab = localStorage.getItem('currentSelectedTab');
 
@@ -66,6 +76,8 @@ const TopBar: React.FC<TopBarType> = (props) => {
 
   const PannelRef = useRef<any>(null);
   const GroupListWrapperRef = useRef<any>(null);
+  const notificationWsRef = useRef<WebSocket | null>(null);
+  const notificationPanelRef = useRef<HTMLDivElement | null>(null);
 
   const _systemTabClickHandle = (item: TopBar.Tab) => {
     //导航栏tab点击
@@ -87,10 +99,17 @@ const TopBar: React.FC<TopBarType> = (props) => {
 
     if (t) {
       let visitedTabs = JSON.parse(t);
-      let index = visitedTabs.findIndex((t: TopBar.Tab) => t.menuId == data.menuId);
-      if (index == -1) {
-        visitedTabs.push(data);
-        localStorage.setItem('visitedTabs', JSON.stringify(visitedTabs));
+      // 确保 visitedTabs 是数组类型,防止 findIndex 报错
+      if (Array.isArray(visitedTabs)) {
+        let index = visitedTabs.findIndex((t: TopBar.Tab) => t.menuId == data.menuId);
+        if (index == -1) {
+          visitedTabs.push(data);
+          localStorage.setItem('visitedTabs', JSON.stringify(visitedTabs));
+        }
+      } else {
+        // 如果不是数组,重新初始化
+        const newVisitedTabs = [data];
+        localStorage.setItem('visitedTabs', JSON.stringify(newVisitedTabs));
       }
     } else {
       localStorage.setItem('visitedTabs', JSON.stringify([data]));
@@ -193,6 +212,198 @@ const TopBar: React.FC<TopBarType> = (props) => {
     }
   };
 
+  const getBaseParams = () => {
+    const userId = initialState?.userData?.userId;
+    let hospId: any;
+    const currentSelectedSubHop_json = localStorage.getItem('currentSelectedSubHop');
+    if (currentSelectedSubHop_json) {
+      try {
+        const parsed = JSON.parse(currentSelectedSubHop_json);
+        hospId = parsed?.id;
+      } catch (e) {
+        hospId = undefined;
+      }
+    }
+    return { hospId, userId };
+  };
+
+  const formatMsgTime = (val: any) => {
+    if (!val) return '';
+    const d = new Date(val);
+    if (Number.isNaN(d.getTime())) return '';
+    return d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).replace(/\//g, '-');
+  };
+
+  const normalizeMessageList = (payload: any): any[] => {
+    if (Array.isArray(payload)) return payload;
+    if (Array.isArray(payload?.data)) return payload.data;
+    if (Array.isArray(payload?.data?.records)) return payload.data.records;
+    if (Array.isArray(payload?.records)) return payload.records;
+    return [];
+  };
+
+  const defaultDateRange = () => {
+    // 默认拉取近30天,避免固定日期导致查不到消息
+    const end = new Date();
+    const start = new Date();
+    start.setDate(end.getDate() - 30);
+    const pad2 = (n: number) => `${n}`.padStart(2, '0');
+    const fmt = (d: Date) =>
+      `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
+    return { receiveBeginTime: fmt(start), receiveEndTime: fmt(end) };
+  };
+
+  const pickMsgId = (item: any): string | number | undefined => {
+    if (!item) return undefined;
+    return item?.id ?? item?.messageId ?? item?.msgId;
+  };
+
+  const renderNotificationItems = (list: any[], isUnread: boolean) => {
+    if (!list || list.length === 0) {
+      return (
+        <div className="notification-empty">
+          <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无消息" />
+        </div>
+      );
+    }
+    return (
+      <div className="notification-list">
+        {list.map((item, index) => {
+          const time = formatMsgTime(item?.receiveTime || item?.createDate || item?.createTime || item?.resolveTime);
+          return (
+            <div className="notification-item" key={pickMsgId(item) || index}>
+              <div className={isUnread ? 'notification-item-icon unread' : 'notification-item-icon read'} />
+              <div className="notification-item-content">
+                <div className="notification-item-title">{item?.title || item?.recordTitle || '消息通知'}</div>
+                <div className="notification-item-desc">{item?.content || item?.description || '您有一条消息,请及时查看'}</div>
+              </div>
+              <div className="notification-item-time">{time}</div>
+            </div>
+          );
+        })}
+      </div>
+    );
+  };
+
+  const fetchNotifications = async (tab: 'unread' | 'read') => {
+    const { hospId } = getBaseParams();
+    if (!hospId) return;
+    const status = tab === 'unread' ? 0 : 1;
+    setNotificationLoading(true);
+    try {
+      const { receiveBeginTime, receiveEndTime } = defaultDateRange();
+      const resp = await getMessageList({
+        status,
+        systemId: KcimCenterSysId,
+        receiveBeginTime,
+        receiveEndTime,
+        resolveBeginTime: '',
+        resolveEndTime: '',
+      });
+      const list = normalizeMessageList(resp);
+      setNotificationList((prev) => ({ ...prev, [tab === 'unread' ? 'unread' : 'read']: list }));
+      if (tab === 'unread') {
+        setUnreadCount(list.length);
+      }
+    } finally {
+      setNotificationLoading(false);
+    }
+  };
+
+  const markAllUnreadAsRead = async () => {
+    isClickInside = true;
+    if (notificationBatchLoading) return;
+
+    const msgIds = notificationList.unread
+      .map(pickMsgId)
+      .filter((id): id is string | number => id !== undefined && id !== null && `${id}`.length > 0);
+
+    if (msgIds.length === 0) {
+      message.info('暂无可标记的未读消息');
+      return;
+    }
+
+    setNotificationBatchLoading(true);
+    try {
+      await batchMessage({
+        msgIds,
+        operation: 'read',
+        systemId: KcimCenterSysId,
+      });
+      setUnreadCount(0);
+      message.success('已全部标记为已读');
+      await fetchNotifications('unread');
+      await fetchNotifications('read');
+    } catch (e) {
+      message.error('标记已读失败,请稍后重试');
+    } finally {
+      setNotificationBatchLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    if (process.env.NODE_ENV === 'development') {
+      // 开发环境暂不建立 WebSocket 连接,避免本地代理异常导致 dev server 崩溃
+      return;
+    }
+    const { hospId, userId } = getBaseParams();
+    if (!hospId || !userId) return;
+
+    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+    const host = window.location.host;
+    // 统一走 /gateway,与 HTTP 请求保持一致,便于本地代理转发
+    let token = '';
+    try {
+      const storageUserData = localStorage.getItem('userData');
+      if (storageUserData) {
+        const parsed = JSON.parse(storageUserData);
+        token = parsed?.token || '';
+      } else if (initialState?.userData?.token) {
+        token = initialState.userData.token;
+      }
+    } catch (_) {
+      token = '';
+    }
+    const tokenQuery = token ? `?token=${encodeURIComponent(token)}` : '';
+    const wsUrl = `${protocol}//${host}/ws-center/centerSys/websocket/unread-count/${hospId}/${userId}${tokenQuery}`;
+
+    const ws = new WebSocket(wsUrl);
+    notificationWsRef.current = ws;
+
+    ws.onmessage = (evt) => {
+      let count = 0;
+      try {
+        const payload = JSON.parse(evt.data);
+        if (typeof payload === 'number') {
+          count = payload;
+        } else if (typeof payload?.unreadCount === 'number') {
+          count = payload.unreadCount;
+        } else if (typeof payload?.count === 'number') {
+          count = payload.count;
+        } else if (typeof payload?.data === 'number') {
+          count = payload.data;
+        }
+      } catch (e) {
+        const num = Number(evt.data);
+        if (!Number.isNaN(num)) count = num;
+      }
+      setUnreadCount(count);
+    };
+
+    ws.onerror = () => {
+      setUnreadCount(0);
+    };
+
+    ws.onclose = () => {
+      setUnreadCount(0);
+    };
+
+    return () => {
+      ws.close();
+      notificationWsRef.current = null;
+    };
+  }, [initialState?.userData?.userId]);
+
   const openNav = () => {
     isClickInside = true;
     setIfOpenPannel(!ifOpenPannel);
@@ -373,6 +584,9 @@ const TopBar: React.FC<TopBarType> = (props) => {
       if (!isClickInside && GroupListWrapperRef.current && !GroupListWrapperRef.current.contains(event.target)) {
         set_showGroupList(false);
       }
+      if (!isClickInside && notificationPanelRef.current && !notificationPanelRef.current.contains(event.target)) {
+        setNotificationModalVisible(false);
+      }
       isClickInside = false;
     };
 
@@ -422,7 +636,7 @@ const TopBar: React.FC<TopBarType> = (props) => {
         </div>
         <span
           className="systemTitle"
-          //  onClick={() => goSystemIndex(pageTitle)}
+        //  onClick={() => goSystemIndex(pageTitle)}
         >
           {initialState?.pageTitle}
         </span>
@@ -463,8 +677,19 @@ const TopBar: React.FC<TopBarType> = (props) => {
           )}
         </>
 
-        <div className="notification">
-          <img className="notificationIcon" src={require('../../../public/images/notificationIcon.png')} />
+        <div
+          className="notification"
+          onClick={() => {
+            isClickInside = true;
+            setNotificationModalVisible(!notificationModalVisible);
+            if (!notificationModalVisible) {
+              fetchNotifications(notificationTab);
+            }
+          }}
+        >
+          <Badge dot={unreadCount > 0} color="#ff4d4f">
+            <img className="notificationIcon" src={require('../../../public/images/notificationIcon.png')} />
+          </Badge>
         </div>
         <div className="group">
           {groupList.length > 0 && (
@@ -552,6 +777,65 @@ const TopBar: React.FC<TopBarType> = (props) => {
           </div>
         </div>
       )}
+
+      {notificationModalVisible && (
+        <div className="notification-panel" ref={notificationPanelRef} onClick={(e) => e.stopPropagation()}>
+          <div className="notification-header">
+            <span className="title">消息记录</span>
+            <img
+              className="close"
+              src={require('../../../public/images/cancel_black.png')}
+              alt=""
+              onClick={() => {
+                isClickInside = true;
+                setNotificationModalVisible(false);
+              }}
+            />
+          </div>
+          <Tabs
+            className="notification-tabs"
+            activeKey={notificationTab}
+            onChange={(key) => {
+              const tab = key === 'read' ? 'read' : 'unread';
+              setNotificationTab(tab);
+              fetchNotifications(tab);
+            }}
+            tabBarExtraContent={
+              <span
+                className={notificationTab === 'unread' && notificationList.unread.length > 0 ? 'mark-all' : 'mark-all disabled'}
+                onClick={(e) => {
+                  e.stopPropagation();
+                  if (notificationTab !== 'unread') return;
+                  if (notificationList.unread.length === 0) return;
+                  markAllUnreadAsRead();
+                }}
+              >
+                {notificationBatchLoading ? '标记中...' : '全部已读'}
+              </span>
+            }
+            items={[
+              {
+                key: 'unread',
+                label: `未读(${notificationList.unread.length})`,
+                children: (
+                  <Spin spinning={notificationLoading}>
+                    {renderNotificationItems(notificationList.unread, true)}
+                  </Spin>
+                ),
+              },
+              {
+                key: 'read',
+                label: `消息`,
+                children: (
+                  <Spin spinning={notificationLoading}>
+                    {renderNotificationItems(notificationList.read, false)}
+                  </Spin>
+                ),
+              },
+            ]}
+          />
+        </div>
+      )}
     </div>
   );
 };

+ 230 - 0
src/components/topBar/style.less

@@ -135,6 +135,228 @@
   }
 }
 
+.notification-modal {
+  .kcmp-ant-modal-body {
+    padding-top: 12px;
+  }
+
+  .notification-list {
+    max-height: 420px;
+    overflow: auto;
+  }
+
+  .notification-item {
+    display: flex;
+    align-items: center;
+    padding: 10px 0;
+    border-bottom: 1px solid #f5f6f8;
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    .notification-item-icon {
+      width: 36px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      margin-right: 8px;
+
+      img {
+        width: 20px;
+        height: 20px;
+      }
+    }
+
+    .notification-item-content {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      gap: 6px;
+
+      .notification-item-title {
+        font-weight: 600;
+        color: #1d2129;
+      }
+
+      .notification-item-desc {
+        color: #4e5969;
+        font-size: 12px;
+      }
+    }
+
+    .notification-item-time {
+      color: #8c8c8c;
+      font-size: 12px;
+      margin-left: 8px;
+      white-space: nowrap;
+    }
+  }
+
+  .notification-empty {
+    padding: 32px 0;
+  }
+}
+
+.notification-panel {
+  position: absolute;
+  top: 52px;
+  right: 100px;
+  width: 440px;
+  max-height: 520px;
+  background: #ffffff;
+  border-radius: 12px;
+  box-shadow: 0 12px 36px rgba(0, 0, 0, 0.16);
+  padding: 0;
+  overflow: hidden;
+  z-index: 1000;
+  display: flex;
+  flex-direction: column;
+
+  .notification-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 12px 16px;
+    background: #f7f9fc;
+    border-radius: 10px 10px 0 0;
+    border-bottom: 1px solid #eef0f3;
+
+    .title {
+      font-size: 16px;
+      font-weight: 600;
+      color: #1d2129;
+    }
+
+    .close {
+      cursor: pointer;
+      width: 16px;
+      height: 16px;
+    }
+  }
+
+  .notification-tabs {
+    padding: 12px 16px 8px 16px;
+
+    .kcmp-ant-tabs-nav {
+      margin-bottom: 8px;
+    }
+
+    .kcmp-ant-tabs-tab {
+      padding: 8px 0;
+      font-weight: 500;
+      color: #1d2129;
+    }
+
+    .kcmp-ant-tabs-ink-bar {
+      background: #3377ff;
+    }
+
+    .kcmp-ant-tabs-tab-active .kcmp-ant-tabs-tab-btn {
+      color: #3377ff;
+    }
+
+    .mark-all {
+      font-size: 14px;
+      color: #1d2129;
+      cursor: pointer;
+      user-select: none;
+      line-height: 32px;
+
+      &.disabled {
+        color: #bfbfbf;
+        cursor: not-allowed;
+      }
+    }
+
+    .kcmp-ant-tabs-extra-content,
+    .ant-tabs-extra-content {
+      display: flex;
+      align-items: center;
+    }
+  }
+
+  .notification-list {
+    max-height: 420px;
+    overflow-y: auto;
+    padding: 8px 16px 8px 16px;
+  }
+
+  .notification-item {
+    display: flex;
+    align-items: center;
+    padding: 10px 0;
+    border-bottom: 1px solid #f0f2f5;
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    .notification-item-icon {
+      width: 40px;
+      height: 40px;
+      border-radius: 50%;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      margin-right: 12px;
+
+      &::before {
+        content: '';
+        width: 20px;
+        height: 20px;
+        background-color: #b2b2b2;
+        -webkit-mask: url('../../../public/images/notificationIcon.png') no-repeat center / contain;
+        mask: url('../../../public/images/notificationIcon.png') no-repeat center / contain;
+      }
+
+      &.unread {
+        background: #fff3e1;
+      }
+
+      &.unread::before {
+        background-color: #f5a623;
+      }
+
+      &.read {
+        background: #eef3fb;
+      }
+
+      &.read::before {
+        background-color: #b2b2b2;
+      }
+    }
+
+    .notification-item-content {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      gap: 6px;
+
+      .notification-item-title {
+        font-weight: 600;
+        color: #1d2129;
+      }
+
+      .notification-item-desc {
+        color: #4e5969;
+        font-size: 12px;
+      }
+    }
+
+    .notification-item-time {
+      color: #8c8c8c;
+      font-size: 12px;
+      margin-left: 8px;
+      white-space: nowrap;
+    }
+  }
+
+  .notification-empty {
+    padding: 32px 0;
+  }
+}
+
 .topBar {
   position: relative;
   z-index: 999;
@@ -606,6 +828,10 @@
             background: url('../../../public/images/appbankuaiguanli.png');
             background-size: contain;
           }
+	          .typeBlockIcon8 {
+	            background: url('../../../public/images/odds_gray.png');
+	            background-size: contain;
+	          }
 
           & > span {
             color: #3376fe;
@@ -646,6 +872,10 @@
             background: url('../../../public/images/appbankuaiguanli.png');
             background-size: contain;
           }
+	          .typeBlockIcon8 {
+	            background: url('../../../public/images/odds.png');
+	            background-size: contain;
+	          }
 
           & > span {
             color: #3376fe;

+ 9 - 1
src/global.less

@@ -171,6 +171,8 @@ input {
 **/
 .kcmp-ant-select {
   .kcmp-ant-select-selector {
+    display: flex;
+    align-items: center; // 统一垂直居中
     // height: 24px !important;
     // padding: 0 8px !important;
     border-radius: 4px !important;
@@ -202,10 +204,16 @@ input {
 
     .kcmp-ant-select-selection-search-input {
       height: 24px !important;
+      line-height: 24px !important; // 与高度保持一致,避免文字下沉
+    }
+
+    .kcmp-ant-select-selection-search {
+      display: flex;
+      align-items: center; // 输入框在容器内垂直居中
     }
 
     .kcmp-ant-select-selection-placeholder {
-      // line-height: 22px !important;
+      line-height: 24px !important; // 与 small 尺寸对齐
       // font-size: 14px;
       // font-family: SourceHanSansCN-Normal, SourceHanSansCN;
       font-weight: 400;

+ 2 - 2
src/global.tsx

@@ -47,7 +47,7 @@ export const logoutHandle = async () => {
 //用于子应用token过期触发登录
 window.addEventListener('removeLocalItemEvent', function (e: EventType) {
   if (e.key == 'userData') {
-    const hospSign = localStorage.getItem('hospSign');
-    history.replace(`${loginPath}?hospSign=${hospSign}`);
+    // 不再自动补齐 hospSign,直接跳转到登录页
+    history.replace(`${loginPath}`);
   }
 });

+ 32 - 11
src/layouts/index.tsx

@@ -11,14 +11,17 @@ import { IRouteComponentProps, useModel, history } from 'umi';
 import ProLayout from '@ant-design/pro-layout';
 
 import TopBar from '@/components/topBar';
+import GlobalErrorBoundary from '@/components/GlobalErrorBoundary';
 import { Key, useEffect, useRef, useState } from 'react';
 import { getSpecifyMenuDetail, getUserPlatformNav } from '@/service/menu';
-import { getAppAccess, getSysParamsByCode } from '@/service';
+import { getAppAccess, getSysParamsByCode, getUserIndexData } from '@/service';
 import { Input, Modal } from 'antd';
 import './style.less';
 import { KcimCenterSysId } from '@/constant';
 import { askAiReq } from '@/service/ai';
 import { createFromIconfontCN } from '@ant-design/icons';
+import { DndProvider } from 'react-dnd';
+import { HTML5Backend } from 'react-dnd-html5-backend';
 import ReactMarkdown from 'react-markdown';
 import AiChat from '@/components/intelligenceBot';
 
@@ -151,7 +154,6 @@ export default function Layout({ children, location, route, history, match }: IR
   const noTopBar = queryParams.get('noTopbar');
   const [ifShowAiBtn, set_ifShowAiBtn] = useState(false);
 
-
   const getNavData = async () => {
     const nav = await getUserPlatformNav();
     if (nav) {
@@ -182,8 +184,6 @@ export default function Layout({ children, location, route, history, match }: IR
     set_emptyPageContent(menuItem.description);
   };
 
-
-
   useEffect(() => {
     if (location.pathname != '/login') {
       getNavData();
@@ -207,6 +207,18 @@ export default function Layout({ children, location, route, history, match }: IR
     }
   });
 
+  useEffect(() => {
+    const userData = JSON.parse(localStorage.getItem('userData') || '{}');
+    const token = userData.token || '';
+    if (token) {
+      getUserIndexData().then(resp => {
+        if (resp && resp.userInfo) {
+          localStorage.setItem('userInfo', JSON.stringify(resp.userInfo));
+        }
+      });
+    }
+  }, []);
+
   if (location.pathname == '/login') {
     return <div>{children}</div>;
   }
@@ -227,6 +239,7 @@ export default function Layout({ children, location, route, history, match }: IR
   // }
 
   return (
+    <DndProvider backend={HTML5Backend}>
     <ProLayout
       layout="top"
       contentStyle={{
@@ -275,16 +288,24 @@ export default function Layout({ children, location, route, history, match }: IR
       }
     >
       {isEmpty && (
-        <div style={{ textAlign: 'center', paddingTop: 100, height: '93vh' }}>
-          <h1>{emptyPageContent}</h1>
-        </div>
+        <GlobalErrorBoundary>
+          <div style={{ textAlign: 'center', paddingTop: 100, height: '93vh' }}>
+            <h1>{emptyPageContent}</h1>
+          </div>
+        </GlobalErrorBoundary>
       )}
       {!isEmpty && (
-        <div className="platform-children" style={{ height: noTopBar == 'true' ? 'auto' : `calc(100vh - 48px)`, minWidth: 1280 }}>
-          {(window.self === window.top&&ifShowAiBtn)&&<AiChat />}
-          <>{children}</>
-        </div>
+        <GlobalErrorBoundary>
+          <div
+            className="platform-children"
+            style={{ height: noTopBar == 'true' ? 'auto' : `calc(100vh - 48px)`, minWidth: 1280 }}
+          >
+            {(window.self === window.top && ifShowAiBtn) && <AiChat />}
+            {children}
+          </div>
+        </GlobalErrorBoundary>
       )}
     </ProLayout>
+    </DndProvider>
   );
 }

+ 41 - 17
src/pages/index/components/FastEntry/index.tsx

@@ -11,7 +11,7 @@
 import { history, useModel } from 'umi';
 import { useEffect, useState } from 'react';
 import './style.less';
-import { Empty, Modal } from 'antd';
+import { Empty, Modal, Spin } from 'antd';
 import NavSelecter, { NavSelecterData, NavSelecterItemType } from '@/components/NavSelecter';
 import { addFastEntry, AddFastEntryDataType, getUserPlatformNav } from '@/service/menu';
 import { getAppAccess, removeFastEntrance } from '@/service';
@@ -41,26 +41,51 @@ export const FastEntry = (props: FastEntryType) => {
   const [tabs, set_tabs] = useState<FastEntryTabType[]>([]);
   const [open, set_open] = useState(false);
   const [navSelecterData, set_navSelecterData] = useState<NavSelecterData[]>([]);
+  const [loading, setLoading] = useState(false);
 
   const tabClickHandle = async (tab: FastEntryTabType) => {
-    const { systemId = '', menuId = '', contentType, type, url } = tab;
-    const resp = await getAppAccess(systemId.length > 0 ? systemId : menuId);
-    if (!resp) {
-      if (contentType != 7 && type != 1) {
-        setInitialState((s: any) => ({ ...s, currentSelectedSys: tab as any, currentTab: tab }));
+    try {
+      setLoading(true);
+      const { systemId = '', menuId = '', contentType, type, url } = tab;
+      const resp = await getAppAccess(systemId.length > 0 ? systemId : menuId);
+      
+      if (!resp) {
+        // 先设置状态
+        setInitialState((s: any) => ({ 
+          ...s, 
+          currentSelectedSys: tab as any, 
+          currentTab: tab,
+          isThirdPartySystem: type === 1 && contentType === 6 
+        }));
+        
+        // 处理iframe系统跳转
+        if (type == 1) {
+          if (contentType == 6) {
+            // 体系或第三方iframe
+            setTimeout(() => {
+              history.push('/platform?noMenu=true');
+            }, 100);
+            return;
+          } else if (contentType == 7) {
+            // 新窗口打开
+            window.open(url, '_blank');
+          }
+        } else {
+          // 普通系统跳转
+          history.push(tab.path);
+        }
+      } else {
+        Modal.error({
+          title: '当前系统未注册,请联系管理员处理!',
+        });
       }
-      // if (type == 1 && contentType == 6) {
-      //   //体系或第三方iframe
-      //   history.push('/platform');
-      // }else if(type == 1 && contentType == 7){
-      //   window.open(url,'_blank');
-      // }else {
-      //   history.push(tab.path);
-      // }
-    } else {
+    } catch (error) {
       Modal.error({
-        title: '当前系统未注册,请联系管理员处理!',
+        title: '系统访问失败',
+        content: '请稍后重试或联系管理员'
       });
+    } finally {
+      setLoading(false);
     }
   };
 
@@ -112,7 +137,6 @@ export const FastEntry = (props: FastEntryType) => {
         })}
         {tabs.length == 0 && (
           <div className="empty">
-            {/* <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} /> */}
             添加系统快速入口,方便再次进入
           </div>
         )}

+ 7 - 0
src/pages/index/components/FastEntry/style.less

@@ -80,5 +80,12 @@
       //    background-size: contain;
       // }
     }
+    .loading {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      width: 100%;
+      height: 100px;
+    }
   }
 }

+ 12 - 13
src/pages/index/components/TodoList/index.tsx

@@ -14,15 +14,15 @@ import './style.less';
 import { createFromIconfontCN } from '@ant-design/icons';
 import '../../../../../public/zhongtaiB.js'
 const IconFont = createFromIconfontCN({
-  scriptUrl:'',
+  scriptUrl: '',
 });
 
 export type todoItem = {
-  recordTitle: string;
-  id: number;
+  messageTypeName: string;
+  msgId: string | number;
   status: number;
-  taskLevel: number;
-  createDate: number;
+  messageLevel: string | number;
+  createTime: string;
   content: string;
 };
 
@@ -36,14 +36,13 @@ export const TodoList = (props: TodoList) => {
   const [todoLists, set_todoLists] = useState<todoItem[]>([]);
 
   const setTodoClass = (val: todoItem) => {
-    if (val.taskLevel == 3) return 'todoStatus green';
-    if (val.taskLevel == 2) return 'todoStatus orange';
-    if (val.taskLevel == 1) return 'todoStatus red';
+    if (val.messageLevel == 3) return 'todoStatus green';
+    if (val.messageLevel == 2) return 'todoStatus orange';
+    if (val.messageLevel == 1) return 'todoStatus red';
   };
 
   const checkBtnHandle = (item: todoItem) => {
-    todoListClickHandle && todoListClickHandle(item.id);
-
+    todoListClickHandle && todoListClickHandle(item.msgId as any);
   };
 
   useEffect(() => {
@@ -54,7 +53,7 @@ export const TodoList = (props: TodoList) => {
     <div className="TodoList">
       <div className="topTitle">
         <span className="name">待办事项</span>
-        <span className='actBtn'><IconFont type='icon-shuaxin' style={{marginRight:4}} />点击刷新</span>
+        <span className='actBtn'><IconFont type='icon-shuaxin' style={{ marginRight: 4 }} />点击刷新</span>
       </div>
       <div className="wrap">
         {todoLists.map((item, index) => {
@@ -62,8 +61,8 @@ export const TodoList = (props: TodoList) => {
             <div className="todoList" key={index}>
               <div className="checkBtn" onClick={() => checkBtnHandle(item)}>去处理</div>
               <div className="status">
-                <div className={setTodoClass(item)}>{item.recordTitle}</div>
-                <span className="date">{item.createDate ? `${item.createDate}`.replace(/:\d{2}$/, '') : ''}</span>
+                <div className={setTodoClass(item)}>{item.messageTypeName}</div>
+                <span className="date">{item.createTime ? `${item.createTime}`.replace(/:\d{2}$/, '') : ''}</span>
               </div>
               <div className="detail" dangerouslySetInnerHTML={{ __html: item.content }}></div>
             </div>

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

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2021-11-10 09:33:30
- * @LastEditTime: 2025-05-20 10:53:28
+ * @LastEditTime: 2025-06-18 11:21:11
  * @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, Tooltip } from 'antd';
+import { Skeleton, Drawer, Tooltip, Button } from 'antd';
 import { useState, useEffect } from 'react';
 
 import { FastEntry, FastEntryTabType } from './components/FastEntry';
@@ -63,6 +63,13 @@ const IndexPage: React.FC<IndexPageType> = ({ location }) => {
   const [welcomTitle, set_welcomTitle] = useState('欢迎进入医管平台');
   const [drawerOpen, set_drawerOpen] = useState(false);
 
+  // 开发环境测试用:触发错误边界
+  const [boom, setBoom] = useState(false);
+  if (boom) {
+    // 抛出渲染期异常,触发 GlobalErrorBoundary
+    throw new Error('测试:全局错误边界效果');
+  }
+
   const onLoadhandle = () => {
     set_iframeLoading(false);
   };
@@ -90,12 +97,12 @@ const IndexPage: React.FC<IndexPageType> = ({ location }) => {
 
       set_todoList(
         resp.todoList.map((t: any) => ({
-          id: t.id,
+          msgId: t.msgId,
           status: 1,
           content: t.content,
-          createDate: t.createDate,
-          taskLevel: t.taskLevel,
-          recordTitle: t.recordTitle,
+          createTime: t.createTime,
+          messageLevel: t.messageLevel,
+          messageTypeName: t.messageTypeName,
         })),
       );
 
@@ -111,7 +118,7 @@ const IndexPage: React.FC<IndexPageType> = ({ location }) => {
     }
   };
 
-  const todoListClickHandle = async (id: number) => {
+  const todoListClickHandle = async (id: string | number) => {
     set_drawerOpen(true);
     // const resp = await todoListAct([id]);
     // if (resp) {
@@ -178,6 +185,16 @@ const IndexPage: React.FC<IndexPageType> = ({ location }) => {
 
   return (
     <div className="indexPage">
+      {/* 开发环境下显示一个触发错误的测试按钮,便于验证错误边界效果 */}
+      {process.env.NODE_ENV === 'development' && (
+        <Button
+          type="primary"
+          onClick={() => setBoom(true)}
+          style={{ position: 'fixed', right: 16, bottom: 96, zIndex: 9999 }}
+        >
+          触发错误页
+        </Button>
+      )}
       {/* <iframe width={1000} height={800} src="http://localhost:8088/superset/dashboard/5b12b583-8204-08e9-392c-422209c29787/?native_filters_key=q3H6TKjSa58" ></iframe> */}
       <Drawer
         className="MsgProcessDrawer"

+ 352 - 50
src/pages/login/index.tsx

@@ -1,14 +1,14 @@
 /*
  * @Author: your name
  * @Date: 2021-11-09 14:58:08
- * @LastEditTime: 2025-03-27 14:13:26
+ * @LastEditTime: 2025-08-26 14:24:34
  * @LastEditors: code4eat awesomedema@gmail.com
  * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  * @FilePath: /KC-MiddlePlatform/src/pages/login/index.tsx
  */
 
 import React, { useRef, useEffect, useState } from 'react';
-import { Select, notification, Row, Col, message } from 'antd';
+import { Select, notification, Row, Col, message, Tabs } from 'antd';
 import './style.less';
 
 import { useModel, history, Location, Helmet } from 'umi';
@@ -17,13 +17,14 @@ import { Form, Input, Button } from 'antd';
 
 import KCSelect from '@/components/kc-select';
 
-import { getHospConfigBySign, getLastLoginSys, login } from '@/service/login';
+import { getHospConfigBySign, getLastLoginSys, login, sendSmsCode, smsLogin } from '@/service/login';
 
 import { KcimCenterSysId } from '@/constant';
-import { getSysParamsByCode } from '@/service';
+import { getSysParamsByCode, getDefaultParam } from '@/service';
 import { ErrorBoundary } from './ErrorBoundary';
 
 const { Option } = Select;
+const { TabPane } = Tabs;
 
 export function changeFavicon(src: string) {
   const link = document.createElement('link');
@@ -72,7 +73,7 @@ export const getHospType = async () => {
   } else {
     return undefined;
   }
-};
+};  
 
 const LoginPage: React.FC<Partial<LoginPageType>> = ({ location = { pathname: '/login' } as Location, children, title = '用户登录' }) => {
   const loginPageRef = useRef<HTMLDivElement>(null);
@@ -86,6 +87,9 @@ const LoginPage: React.FC<Partial<LoginPageType>> = ({ location = { pathname: '/
       loginPic?: string;
       loginTips?: string;
       systemName?: string;
+      loginMethod?: string;
+      loginNameDisplayStyle?: string; // 医院名称显示方式:'0'-显示systemName文本,'1'-显示loginNamePicUrl图片
+      loginNamePicUrl?: string; // 医院名称图片URL
     }[]
   >([]); // 分院列表
   const [ifLoading, setIfLoading] = useState(false);
@@ -97,10 +101,35 @@ const LoginPage: React.FC<Partial<LoginPageType>> = ({ location = { pathname: '/
     id: undefined,
     loginTips: '',
     loginPic: '',
+    loginMethod: '',
+    loginNameDisplayStyle: '0',
+    loginNamePicUrl: '',
   }); // 当前选中的院区
+  
+  const [currentLoginType, setCurrentLoginType] = useState<string>('password'); // 当前登录方式:sms|password
+  const [smsCodeCountdown, setSmsCodeCountdown] = useState<number>(0); // 短信验证码倒计时
+  const [smsForm] = Form.useForm(); // 短信登录表单实例
+  const phoneInputRef = useRef<HTMLInputElement>(null); // 手机号输入框引用
 
   const [hospSign, set_hospSign] = useState<undefined | string>(undefined); // 登陆用的hospSign
 
+  // 解析登录方式配置
+  const parseLoginMethod = (loginMethod: string) => {
+    if (!loginMethod) return { sms: 0, password: 1, defaultType: 'password' };
+    
+    const methods = loginMethod.split('|');
+    const sms = parseInt(methods[0]) || 0;
+    const password = parseInt(methods[1]) || 0;
+    
+    let defaultType = 'password';
+    if (sms === 2) defaultType = 'sms';
+    else if (password === 2) defaultType = 'password';
+    else if (sms === 1 && password === 0) defaultType = 'sms';
+    else if (password === 1 && sms === 0) defaultType = 'password';
+    
+    return { sms, password, defaultType };
+  };
+
   // 获取当前账号分院列表
   const getSubHospFunc = async () => {
     if (hospSign) {
@@ -117,6 +146,9 @@ const LoginPage: React.FC<Partial<LoginPageType>> = ({ location = { pathname: '/
               loginPic: t.loginPic ? t.loginPic : '',
               loginTips: t.loginTips ? t.loginTips : '',
               systemName: t.systemName,
+              loginMethod: (t as any).loginMethod ? (t as any).loginMethod : '',
+              loginNameDisplayStyle: (t as any).loginNameDisplayStyle || '0', // 默认显示文本
+              loginNamePicUrl: (t as any).loginNamePicUrl || '', // 医院名称图片URL
             };
             if (t.hospSign == hospSign) {
               setCurrentHospName(temp.systemName || title);
@@ -124,6 +156,10 @@ const LoginPage: React.FC<Partial<LoginPageType>> = ({ location = { pathname: '/
               localStorage.setItem('currentSelectedSubHop', JSON.stringify(temp));
               localStorage.setItem('hospAbbreviation', temp.hospAbbreviation);
               localStorage.setItem('currentHospName', temp.systemName || '');
+              
+              // 设置默认登录方式
+              const loginConfig = parseLoginMethod(temp.loginMethod);
+              setCurrentLoginType(loginConfig.defaultType);
             }
             return temp;
           }),
@@ -171,11 +207,27 @@ const LoginPage: React.FC<Partial<LoginPageType>> = ({ location = { pathname: '/
 
     window.addEventListener('resize', (e) => handleResize(e)); // 监听窗口大小改变
 
+    // 优先从URL中获取 hospSign;若无,则通过默认参数接口兜底获取
     const queryHospSign = history.location.query?.hospSign as string;
-    localStorage.setItem('hospSign', queryHospSign);
-    set_hospSign(queryHospSign);
-    if (!queryHospSign) {
-      message.error('网址格式不正确,请添加正确的链接hospSign标识!');
+    if (queryHospSign) {
+      localStorage.setItem('hospSign', queryHospSign);
+      set_hospSign(queryHospSign);
+    } else {
+      // 通过默认参数接口获取当前院区医院标识
+      (async () => {
+        try {
+          const resp = await getDefaultParam('1547394914533380096', '1960276163221917696');
+          const defaultHospSign = (resp as any)?.value;
+          if (defaultHospSign) {
+            localStorage.setItem('hospSign', defaultHospSign);
+            set_hospSign(defaultHospSign);
+          } else {
+            message.error('网址格式不正确,请添加正确的链接hospSign标识!');
+          }
+        } catch (error) {
+          message.error('获取默认hospSign失败,请检查网络!');
+        }
+      })();
     }
 
     return () => {
@@ -207,8 +259,122 @@ const LoginPage: React.FC<Partial<LoginPageType>> = ({ location = { pathname: '/
     }
   };
 
+  // 短信登录提交
+  const onSmsFinish = async (values: { phone: string; code: string }) => {
+    setIfLoading(true);
+    
+    try {
+      if (!hospSign) {
+        message.error('网址标记缺失,请检查网址!');
+        setIfLoading(false);
+        return;
+      }
+
+      // 调用短信登录接口
+      const resp = await smsLogin({
+        account: values.phone,
+        hospSign: hospSign,
+        password: values.code, // 验证码作为password参数
+      });
+
+      if (resp) {
+        localStorage.setItem('userData', JSON.stringify(resp));
+        localStorage.setItem('account', values.phone);
+        localStorage.setItem('tokenExpired', 'false');
+        
+        const currentSelectedSubHopStr = localStorage.getItem('currentSelectedSubHop') || '{}';
+        const currentSelectedSubHop = JSON.parse(currentSelectedSubHopStr);
+        const customerType = await getHospType();
+        setInitialState((s: any) => ({ ...(s || {}), userData: resp, customerType }));
+        localStorage.setItem('customerType', customerType || '');
+
+        if (currentSelectedSubHop.loadType) {
+          const lastLoginSysData = await getLastLoginSys({ hospId: currentSelectedSubHop.id, userId: resp.userId });
+          if (lastLoginSysData) {
+            history.replace(lastLoginSysData.path);
+            localStorage.setItem('currentSelectedTab', JSON.stringify({ ...lastLoginSysData, menuId: lastLoginSysData.systemId }));
+          }
+        } else {
+          history.replace('/index');
+        }
+      }
+    } catch (error) {
+      // 登录失败的错误信息已经由全局 errorHandler 处理
+      console.error('短信登录失败:', error);
+    } finally {
+      setIfLoading(false);
+    }
+  };
+
+  // 获取验证码
+  const getVerifyCode = async () => {
+    try {
+      // 直接获取手机号,不依赖表单验证
+      let phone = smsForm.getFieldValue('phone');
+      
+      // 如果从表单获取不到,尝试从DOM获取
+      if (!phone) {
+        const phoneInputs = document.querySelectorAll('input[placeholder="请输入手机号"]');
+        for (let input of phoneInputs) {
+          const inputElement = input as HTMLInputElement;
+          if (inputElement.value) {
+            phone = inputElement.value;
+            // 同时更新表单的值
+            smsForm.setFieldsValue({ phone });
+            break;
+          }
+        }
+      }
+      
+      if (!phone) {
+        message.error('请先输入手机号');
+        return;
+      }
+      
+      // 手机号格式验证
+      const phoneRegex = /^1[3-9]\d{9}$/;
+      if (!phoneRegex.test(phone)) {
+        message.error('请输入正确的手机号');
+        return;
+      }
+      
+      if (!hospSign) {
+        message.error('网址标记缺失,请检查网址!');
+        return;
+      }
+      
+      // 发送短信验证码
+      const result = await sendSmsCode({
+        account: phone,
+        hospSign: hospSign
+      });
+      // 检查返回结果,只有真正成功时才显示成功提示和启动倒计时
+      if (result) {       
+        // 开始倒计时
+        setSmsCodeCountdown(60);
+        const timer = setInterval(() => {
+          setSmsCodeCountdown((prev) => {
+            if (prev <= 1) {
+              clearInterval(timer);
+              return 0;
+            }
+            return prev - 1;
+          });
+        }, 1000);
+      }
+      // 如果 success 为 false,错误信息已经由全局 errorHandler 处理了
+      
+    } catch (error: any) {
+      // 网络错误等异常情况
+      console.error('发送验证码失败:', error);
+    }
+  };
+
   const loginPicArr = currentSelectedSubHop?.loginPic ? currentSelectedSubHop.loginPic.split('|') : [];
   const loginTipsArr = currentSelectedSubHop?.loginTips ? currentSelectedSubHop.loginTips.split('|') : [];
+  const hasBeianTextSlot = loginTipsArr.length >= 5;
+  const beianIconUrl = hasBeianTextSlot ? loginTipsArr[3] : '';
+  const beianText = hasBeianTextSlot ? loginTipsArr[4] : loginTipsArr[3];
 
   return (
     <div className="loginPage">
@@ -242,51 +408,187 @@ const LoginPage: React.FC<Partial<LoginPageType>> = ({ location = { pathname: '/
                   </div>
                 )}
 
-                <div className="subHospSelector">
-                  {subHospList.length > 0 && (
-                    <KCSelect
-                      allowClear={false}
-                      style={{ width: 180 }}
-                      defaultValue={hospSign}
-                      onSelect={(val: any, option) => {
-                        set_hospSign(val);
-                        localStorage.setItem('hospAbbreviation', option.item.hospAbbreviation);
-                        localStorage.setItem('currentSelectedSubHop', JSON.stringify(option.item));
-                        set_currentSelectedSubHop(option.item);
-                      }}
-                      suffixIcon={<img style={{ width: '10px', height: '6px' }} src={require('../../../public/images/arrow.png')} />}
-                    >
-                      {subHospList.map((item) => {
+                <div className="loginFormContainer">
+                  <div className="subHospSelector">
+                    {subHospList.length > 0 && (
+                      <KCSelect
+                        allowClear={false}
+                        style={{ width: 180}}
+                        defaultValue={hospSign}
+                        onSelect={(val: any, option) => {
+                          set_hospSign(val);
+                          localStorage.setItem('hospAbbreviation', option.item.hospAbbreviation);
+                          localStorage.setItem('currentSelectedSubHop', JSON.stringify(option.item));
+                          set_currentSelectedSubHop(option.item);
+                          
+                          // 更新医院名称显示
+                          setCurrentHospName(option.item.systemName || title);
+                          localStorage.setItem('currentHospName', option.item.systemName || '');
+                          
+                          // 更新登录方式
+                          const loginConfig = parseLoginMethod(option.item.loginMethod);
+                          setCurrentLoginType(loginConfig.defaultType);
+                        }}
+                        suffixIcon={<img style={{ width: '16px', height: '16px' }} src={require('../../../public/images/login_arrow_white.png')} />}
+                      >
+                        {subHospList.map((item) => {
+                          return (
+                            <Option value={item.value} item={item} key={item.value}>
+                              {item.name}
+                            </Option>
+                          );
+                        })}
+                      </KCSelect>
+                    )}
+                  </div>
+                  <div className={`systemName ${currentSelectedSubHop?.loginNameDisplayStyle === '1' && currentSelectedSubHop?.loginNamePicUrl ? 'has-image' : ''}`}>
+                    {currentSelectedSubHop?.loginNameDisplayStyle === '1' && currentSelectedSubHop?.loginNamePicUrl ? (
+                      <img 
+                        src={currentSelectedSubHop.loginNamePicUrl} 
+                        alt={currentHospName}
+                      />
+                    ) : (
+                      currentHospName
+                    )}
+                  </div>
+                  
+                  {(() => {
+                    const loginConfig = parseLoginMethod(currentSelectedSubHop?.loginMethod || '');
+                    const showTabs = (loginConfig.sms > 0 && loginConfig.password > 0);
+                    
+                    if (!showTabs) {
+                      // 只有一种登录方式,不显示Tab
+                      if (loginConfig.sms > 0) {
                         return (
-                          <Option value={item.value} item={item} key={item.value}>
-                            {item.name}
-                          </Option>
+                          <Form form={smsForm} onFinish={onSmsFinish}>
+                            <Form.Item name="phone" rules={[{ required: true, message: '请输入手机号!' }]}>
+                              <Input className="input" placeholder="请输入手机号" />
+                            </Form.Item>
+
+                            <Form.Item name="code" rules={[{ required: true, message: '请输入验证码!' }]}>
+                              <Input 
+                                className="input" 
+                                placeholder="请输入验证码" 
+                                suffix={
+                                  <Button 
+                                    type="link" 
+                                    onClick={getVerifyCode} 
+                                    disabled={smsCodeCountdown > 0}
+                                    style={{ padding: 0, color: smsCodeCountdown > 0 ? '#ccc' : '#3377ff' }}
+                                  >
+                                    {smsCodeCountdown > 0 ? `${smsCodeCountdown}s` : '获取验证码'}
+                                  </Button>
+                                }
+                              />
+                            </Form.Item>
+
+                            <Form.Item>
+                              <Button className="loginBtn" type="primary" htmlType="submit" loading={ifLoading}>
+                                登录
+                              </Button>
+                            </Form.Item>
+                          </Form>
                         );
-                      })}
-                    </KCSelect>
-                  )}
+                      } else {
+                        return (
+                          <Form onFinish={onFinish}>
+                            <Form.Item name="account" rules={[{ required: true, message: '请输入用户名!' }]}>
+                              <Input className="input" placeholder="请输入用户名" />
+                            </Form.Item>
+
+                            <Form.Item name="password" rules={[{ required: true, message: '请输入密码!' }]}>
+                              <Input.Password className="input" placeholder="请输入密码" />
+                            </Form.Item>
+
+                            <Form.Item>
+                              <Button className="loginBtn" type="primary" htmlType="submit" loading={ifLoading}>
+                                登录
+                              </Button>
+                            </Form.Item>
+                          </Form>
+                        );
+                      }
+                    }
+                    
+                    // 显示Tab切换
+                    return (
+                      <div className="loginTabs">
+                        <Tabs 
+                          activeKey={currentLoginType} 
+                          onChange={setCurrentLoginType}
+                        >
+                          {loginConfig.sms > 0 && (
+                            <TabPane tab="短信登录" key="sms">
+                              <Form form={smsForm} onFinish={onSmsFinish}>
+                                <Form.Item name="phone" rules={[{ required: true, message: '请输入手机号!' }]}>
+                                  <Input className="input" placeholder="请输入手机号" />
+                                </Form.Item>
+
+                                <Form.Item name="code" rules={[{ required: true, message: '请输入验证码!' }]}>
+                                  <Input 
+                                    className="input" 
+                                    placeholder="请输入验证码" 
+                                    suffix={
+                                      <Button 
+                                        type="link" 
+                                        onClick={getVerifyCode} 
+                                        disabled={smsCodeCountdown > 0}
+                                        style={{ padding: 0, color: smsCodeCountdown > 0 ? '#ccc' : '#3377ff' }}
+                                      >
+                                        {smsCodeCountdown > 0 ? `${smsCodeCountdown}s` : '获取验证码'}
+                                      </Button>
+                                    }
+                                  />
+                                </Form.Item>
+
+                                <Form.Item>
+                                  <Button className="loginBtn" type="primary" htmlType="submit" loading={ifLoading}>
+                                    登录
+                                  </Button>
+                                </Form.Item>
+                              </Form>
+                            </TabPane>
+                          )}
+                          
+                          {loginConfig.password > 0 && (
+                            <TabPane tab="密码登录" key="password">
+                              <Form onFinish={onFinish}>
+                                <Form.Item name="account" rules={[{ required: true, message: '请输入用户名!' }]}>
+                                  <Input className="input" placeholder="请输入用户名" />
+                                </Form.Item>
+
+                                <Form.Item name="password" rules={[{ required: true, message: '请输入密码!' }]}>
+                                  <Input.Password className="input" placeholder="请输入密码" />
+                                </Form.Item>
+
+                                <Form.Item>
+                                  <Button className="loginBtn" type="primary" htmlType="submit" loading={ifLoading}>
+                                    登录
+                                  </Button>
+                                </Form.Item>
+                              </Form>
+                            </TabPane>
+                          )}
+                        </Tabs>
+                      </div>
+                    );
+                  })()}
                 </div>
-                <div className="systemName">{currentHospName}</div>
-                <Form onFinish={onFinish}>
-                  <Form.Item name="account" rules={[{ required: true, message: '请输入用户名!' }]}>
-                    <Input className="input" placeholder="请输入用户名" />
-                  </Form.Item>
-
-                  <Form.Item name="password" rules={[{ required: true, message: '请输入密码!' }]}>
-                    <Input.Password className="input" placeholder="请输入密码" />
-                  </Form.Item>
-
-                  <Form.Item>
-                    <Button className="loginBtn" type="primary" htmlType="submit" loading={ifLoading}>
-                      登录
-                    </Button>
-                  </Form.Item>
-                </Form>
                 <div className="bottomCopyright">
-                  {loginTipsArr[1]}
-                  <span style={{ paddingLeft: 16 }}>{loginTipsArr[2]}</span>
-                  <a target="_blank" href="https://beian.miit.gov.cn/" style={{ display: 'inline-block', textDecoration: 'none', marginLeft: 10, color: '#515866' }} rel="noreferrer">
-                    滇ICP备2024031095号
+                  <span className="copyrightText">
+                    {loginTipsArr[1]}
+                    <span style={{ paddingLeft: 16 }}>{loginTipsArr[2]}</span>
+                  </span>
+                  <a
+                    className="beianLink"
+                    target="_blank"
+                    href="https://beian.miit.gov.cn/"
+                    style={{ textDecoration: 'none', marginLeft: 10, color: '#515866' }}
+                    rel="noreferrer"
+                  >
+                    {/* 滇ICP备2024031095号 */}
+                    {beianIconUrl && <img className="beianIcon" src={beianIconUrl} alt="警徽" />}
+                    {beianText}
                   </a>
                 </div>
               </div>

+ 108 - 10
src/pages/login/style.less

@@ -54,6 +54,64 @@
     justify-content: center;
     align-items: center;
 
+    .loginFormContainer {
+      display: flex;
+      flex-direction: column;
+      align-items: flex-start;
+      
+      .loginTabs {
+        width: 100%;
+        
+        .kcmp-ant-tabs {
+          .kcmp-ant-tabs-nav {
+            margin-bottom: 24px;
+            
+            &::before {
+              display: none; // 去掉灰色底部横线
+            }
+            
+            .kcmp-ant-tabs-nav-wrap {
+              justify-content: flex-start; // 左对齐
+              
+              .kcmp-ant-tabs-nav-list {
+                .kcmp-ant-tabs-tab {
+                  font-size: 16px;
+                  font-weight: 400;
+                  color:#7B8599;
+                  padding: 8px 0;
+                  padding-top: 0;
+                  &:nth-child(2) {
+                    margin-right: 0; // 最后一个tab不需要右边距
+                    margin-left: 16px !important;
+                  }
+                  
+                  &.kcmp-ant-tabs-tab-active {
+                    color: #1a2233;
+                    .kcmp-ant-tabs-tab-btn {
+                      color: #1a2233;
+                    }
+                  }
+                }
+                
+                .kcmp-ant-tabs-ink-bar {
+                  background: #1a2233; // 蓝色下划线换成和激活字体一个颜色
+                  height: 2px;
+                }
+              }
+            }
+          }
+          
+          .kcmp-ant-tabs-content-holder {
+            .kcmp-ant-tabs-content {
+              .kcmp-ant-tabs-tabpane {
+                padding: 0;
+              }
+            }
+          }
+        }
+      }
+    }
+
     .topLogo {
       position: absolute;
       top: 40px;
@@ -80,14 +138,14 @@
     }
 
     .subHospSelector {
-      margin-bottom: 24px;
+      margin-bottom: 16px;
 
       .kcmp-ant-select .kcmp-ant-select-selector {
-        border-radius: 16px !important;
-        padding-left: 20px !important;
-        background: #F5F7FA;
+        border-radius: 4px !important;
+        padding-left: 12px !important;
+        background: #F5F7FA !important;
         height: 32px !important;
-
+        border:none !important;
         .kcmp-ant-select-selection-item {
             line-height:29px !important;
         }
@@ -97,16 +155,32 @@
 
     .systemName {
       font-size: 32px;
-      height: 32px;
       line-height: 32px;
       font-family: SourceHanSansCN-Light, SourceHanSansCN;
       font-weight: 300;
       color: #1a2233;
-      margin-bottom: 32px;
+      display: flex;
+      align-items: center;
+      justify-content: flex-start;
+      width: 320px; // 统一宽度为320px
+      margin-bottom: 38px; // 默认文字时的下边距
+      
+      // 当显示图片时的样式调整
+      &.has-image {
+        margin-bottom: 24px; // 图片时的下边距
+      }
+      
+      // 医院名称图片样式
+      img {
+        width: 320px;
+        height: 64px;
+        object-fit: contain;
+        display: block;
+      }
     }
 
     .input {
-      width: 280px;
+      width: 320px; // 统一宽度为320px
       height: 40px;
       background: #ffffff;
       border-radius: 4px;
@@ -114,10 +188,11 @@
     }
 
     .loginBtn {
-      width: 280px;
+      width: 320px; // 统一宽度为320px
       height: 40px;
       background: #3377ff;
       border-radius: 4px;
+      margin-top: 8px;
     }
 
     .checkBtn {
@@ -155,21 +230,44 @@
     }
 
     .kcmp-ant-select-selection-item {
-      font-size: 14px;
+      font-size: 12px;
       font-family: SourceHanSansCN-Normal, SourceHanSansCN;
       font-weight: 400;
       color: #666f80;
     }
 
+    // 下拉选项的字体大小
+    .kcmp-ant-select-dropdown .kcmp-ant-select-item {
+      font-size: 12px;
+    }
+
     .bottomCopyright {
       position: absolute;
+      left:1%;
       bottom: 25px;
       width: 100%;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      flex-wrap: wrap;
       text-align: center;
       font-size: 12px;
       font-family: SourceHanSansCN-Normal, SourceHanSansCN;
       font-weight: 400;
       color: #515866;
+      .copyrightText {
+        display: inline-flex;
+        align-items: center;
+      }
+      .beianLink {
+        display: inline-flex;
+        align-items: center;
+      }
+      .beianIcon {
+        width: 14px;
+        height: 14px;
+        margin-right: 4px;
+      }
     }
 
     .versionInfo {

+ 63 - 199
src/pages/personalCenter/components/base.tsx

@@ -1,3 +1,11 @@
+/*
+ * @Author: code4eat awesomedema@gmail.com
+ * @Date: 2022-05-25 14:45:47
+ * @LastEditors: code4eat awesomedema@gmail.com
+ * @LastEditTime: 2025-06-16 18:11:33
+ * @FilePath: /KC-MiddlePlatform/src/pages/personalCenter/components/base.tsx
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ */
 import React from 'react';
 import { UploadOutlined } from '@ant-design/icons';
 import { Button, Input, Upload, message } from 'antd';
@@ -8,9 +16,6 @@ import ProForm, {
   ProFormText,
   ProFormTextArea,
 } from '@ant-design/pro-form';
-import { useRequest } from 'umi';
-import { queryCurrent } from '../service';
-import { queryProvince, queryCity } from '../service';
 
 import styles from './BaseView.less';
 
@@ -25,209 +30,68 @@ const validatorPhone = (rule: any, value: string, callback: (message?: string) =
   callback();
 };
 // 头像组件 方便以后独立,增加裁剪之类的功能
-const AvatarView = ({ avatar }: { avatar: string }) => (
-  <>
-    <div className={styles.avatar_title}>头像</div>
-    <div className={styles.avatar}>
-      <img src={avatar} alt="avatar" />
-    </div>
-    <Upload showUploadList={false}>
-      <div className={styles.button_view}>
-        <Button>
-          <UploadOutlined />
-          更换头像
-        </Button>
+const AvatarView = ({ avatar, setAvatarUrl }: { avatar: string; setAvatarUrl: (url: string) => void }) => {
+  const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
+  const userData = JSON.parse(localStorage.getItem('userData') || '{}');
+  const token = userData.token || '';
+  const userId = userInfo.id;
+  return (
+    <>
+      <div className={styles.avatar_title}>头像</div>
+      <div className={styles.avatar}>
+        <img src={avatar || '/images/avatar.png'} alt="avatar" />
       </div>
-    </Upload>
-  </>
-);
+      <Upload
+        name="file"
+        showUploadList={false}
+        accept=".jpg,.jpeg,.png"
+        action="/gateway/centerSys/user/updateAvatar"
+        headers={{ token }}
+        data={{ userId }}
+        onChange={info => {
+          if (info.file.status === 'done') {
+            if (info.file.response && (info.file.response.code === 0 || info.file.response.status === 200)) {
+              message.success('上传成功');
+              // 只用本地图片做预览
+              if (info.file.originFileObj) {
+                const localUrl = URL.createObjectURL(info.file.originFileObj);
+                setAvatarUrl(localUrl);
+              }
+            } else {
+              message.error(info.file.response?.msg || '上传失败');
+            }
+          } else if (info.file.status === 'error') {
+            message.error('上传失败');
+          }
+        }}
+      >
+        <div className={styles.button_view}>
+          <Button>
+            <UploadOutlined />
+            更换头像
+          </Button>
+        </div>
+      </Upload>
+    </>
+  );
+};
 
 const BaseView: React.FC = () => {
-  const { data: currentUser, loading } = useRequest(() => {
-    return queryCurrent();
-  });
-
-  const getAvatarURL = () => {
-    if (currentUser) {
-      if (currentUser.avatar) {
-        return currentUser.avatar;
-      }
-      const url = 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png';
-      return url;
-    }
-    return '';
+  const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
+  const [avatarUrl, setAvatarUrl] = React.useState(userInfo.avatarUrl || '');
+  const refreshAvatar = () => {
+    const newUserInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
+    setAvatarUrl((newUserInfo.avatarUrl || '') + '?t=' + Date.now());
   };
 
-  const handleFinish = async () => {
-    message.success('更新基本信息成功');
-  };
   return (
     <div className={styles.baseView}>
-      {loading ? null : (
-        <>
-          <div className={styles.left}>
-            <ProForm
-              layout="vertical"
-              onFinish={handleFinish}
-              submitter={{
-                resetButtonProps: {
-                  style: {
-                    display: 'none',
-                  },
-                },
-                submitButtonProps: {
-                  children: '更新基本信息',
-                },
-              }}
-              initialValues={{
-                ...currentUser,
-                phone: currentUser?.phone.split('-'),
-              }}
-              hideRequiredMark
-            >
-              <ProFormText
-                width="md"
-                name="email"
-                label="邮箱"
-                rules={[
-                  {
-                    required: true,
-                    message: '请输入您的邮箱!',
-                  },
-                ]}
-              />
-              <ProFormText
-                width="md"
-                name="name"
-                label="昵称"
-                rules={[
-                  {
-                    required: true,
-                    message: '请输入您的昵称!',
-                  },
-                ]}
-              />
-              <ProFormTextArea
-                name="profile"
-                label="个人简介"
-                rules={[
-                  {
-                    required: true,
-                    message: '请输入个人简介!',
-                  },
-                ]}
-                placeholder="个人简介"
-              />
-              <ProFormSelect
-                width="sm"
-                name="country"
-                label="国家/地区"
-                rules={[
-                  {
-                    required: true,
-                    message: '请输入您的国家或地区!',
-                  },
-                ]}
-                options={[
-                  {
-                    label: '中国',
-                    value: 'China',
-                  },
-                ]}
-              />
-
-              <ProForm.Group title="所在省市" size={8}>
-                <ProFormSelect
-                  rules={[
-                    {
-                      required: true,
-                      message: '请输入您的所在省!',
-                    },
-                  ]}
-                  width="sm"
-                  fieldProps={{
-                    labelInValue: true,
-                  }}
-                  name="province"
-                  className={styles.item}
-                  request={async () => {
-                    return queryProvince().then(({ data }) => {
-                      return data.map((item) => {
-                        return {
-                          label: item.name,
-                          value: item.id,
-                        };
-                      });
-                    });
-                  }}
-                />
-                <ProFormDependency name={['province']}>
-                  {({ province }) => {
-                    return (
-                      <ProFormSelect
-                        params={{
-                          key: province?.value,
-                        }}
-                        name="city"
-                        width="sm"
-                        rules={[
-                          {
-                            required: true,
-                            message: '请输入您的所在城市!',
-                          },
-                        ]}
-                        disabled={!province}
-                        className={styles.item}
-                        request={async () => {
-                          if (!province?.key) {
-                            return [];
-                          }
-                          return queryCity(province.key || '').then(({ data }) => {
-                            return data.map((item) => {
-                              return {
-                                label: item.name,
-                                value: item.id,
-                              };
-                            });
-                          });
-                        }}
-                      />
-                    );
-                  }}
-                </ProFormDependency>
-              </ProForm.Group>
-              <ProFormText
-                width="md"
-                name="address"
-                label="街道地址"
-                rules={[
-                  {
-                    required: true,
-                    message: '请输入您的街道地址!',
-                  },
-                ]}
-              />
-              <ProFormFieldSet
-                name="phone"
-                label="联系电话"
-                rules={[
-                  {
-                    required: true,
-                    message: '请输入您的联系电话!',
-                  },
-                  { validator: validatorPhone },
-                ]}
-              >
-                <Input className={styles.area_code} />
-                <Input className={styles.phone_number} />
-              </ProFormFieldSet>
-            </ProForm>
-          </div>
-          <div className={styles.right}>
-            <AvatarView avatar={getAvatarURL()} />
-          </div>
-        </>
-      )}
+      <div className={styles.left}>
+        {/* 只保留头像上传区域,隐藏其它表单项 */}
+      </div>
+      <div className={styles.right}>
+        <AvatarView avatar={avatarUrl} setAvatarUrl={setAvatarUrl} />
+      </div>
     </div>
   );
 };

+ 64 - 22
src/pages/personalCenter/components/security.tsx

@@ -12,7 +12,7 @@ import KCModal from '@/components/KCModal';
 import { ProFormText } from '@ant-design/pro-form';
 import { Form, Alert, Progress, notification, Modal } from 'antd';
 import PasswordQualityCalculator from 'password-quality-calculator';
-import { editUsers, editUsersPsd } from '@/service/user';
+import { editUsers, editUsersPsd, checkPassword } from '@/service/user';
 import { useModel } from 'umi';
 
 type Unpacked<T> = T extends (infer U)[] ? U : T;
@@ -66,6 +66,7 @@ const SecurityView: React.FC = () => {
   const [modalVisible, setmodalVisible] = useState(false);
   const [passwordStrength, setpasswordStrength] = useState(0);
   const [showPasswordCompareTip, setshowPasswordCompareTip] = useState(false);
+  const [showOldPasswordErrorTip, setshowOldPasswordErrorTip] = useState(false);
 
   const data = getData();
 
@@ -83,7 +84,7 @@ const SecurityView: React.FC = () => {
   };
 
   const onFinishhandle = async (values: any) => {
-    const { newPassword, reNewPassword } = values;
+    const { oldPassword, newPassword, reNewPassword } = values;
 
     if (reNewPassword != newPassword) {
       setshowPasswordCompareTip(true);
@@ -92,33 +93,62 @@ const SecurityView: React.FC = () => {
       }, 2000);
       return false;
     }
+    // 新旧密码一致时,确保隐藏上次可能遗留的提示
+    setshowPasswordCompareTip(false);
+    setshowOldPasswordErrorTip(false);
     const userData = localStorage.getItem('userData');
 
-    if (userData) {
-      const resp = await editUsersPsd({
-        id: JSON.parse(userData).userId,
-        password: newPassword,
+    if (!userData) {
+      notification['error']({
+        message: '用户信息获取错误!',
       });
+      return false;
+    }
+
+    const userId = JSON.parse(userData).userId;
+
+    // 先校验原密码
+    try {
+      const checkResp = await checkPassword({ userId, password: oldPassword });
+      // 按拦截器返回布尔值处理
+      const isValid = !!checkResp;
 
-      if (resp) {
-        Modal.confirm({
-          title: '密码修改成功,前往重新登录?',
-          closable: false,
-          okText:'确定',
-          onOk: () => {
-            if (initialState?.logout) {
-              initialState.logout();
-            }
-          },
-          cancelText: '',
-        });
-        return true;
+      if (!isValid) {
+        setshowOldPasswordErrorTip(true);
+        setTimeout(() => {
+          setshowOldPasswordErrorTip(false);
+        }, 2000);
+        return false;
       }
-    } else {
-      notification['error']({
-        message: '用户信息获取错误!',
+    } catch (e) {
+      setshowOldPasswordErrorTip(true);
+      setTimeout(() => {
+        setshowOldPasswordErrorTip(false);
+      }, 2000);
+      return false;
+    }
+
+    const resp = await editUsersPsd({
+      id: userId,
+      password: newPassword,
+    });
+
+    if (resp) {
+      Modal.confirm({
+        title: '密码修改成功,前往重新登录?',
+        closable: false,
+        okText:'确定',
+        onOk: () => {
+          if (initialState?.logout) {
+            initialState.logout();
+          }
+        },
+        cancelText: '',
       });
+      return true;
     }
+
+    return false;
   };
 
   return (
@@ -134,6 +164,18 @@ const SecurityView: React.FC = () => {
         onFinish={(val) => onFinishhandle(val)}
       >
         <div style={{ position: 'relative', paddingTop: '24px' }}>
+          <Alert
+            message="原密码不正确!"
+            type="error"
+            showIcon
+            style={{
+              position: 'absolute',
+              marginBottom: '16px',
+              top: showOldPasswordErrorTip ? -27 : -70,
+              transition: 'all 0.3s ease-in',
+              width: '100%',
+            }}
+          />
           <Alert
             message="两次输入密码不一致!"
             type="error"

+ 1 - 1
src/pages/personalCenter/index.tsx

@@ -18,7 +18,7 @@ type PAGE_NAME_UPPER_CAMEL_CASEState = {
 
 const PAGE_NAME_UPPER_CAMEL_CASE: React.FC = () => {
   const menuMap: Record<string, React.ReactNode> = {
-    // base: '基本设置',
+    base: '基本设置',
     security: '安全设置',
     // binding: '账号绑定',
     notification: '消息',

+ 8 - 36
src/pages/platform/_layout.tsx

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2022-01-06 15:25:39
- * @LastEditTime: 2025-05-22 14:30:28
+ * @LastEditTime: 2025-06-18 13:10:16
  * @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
@@ -232,21 +232,9 @@ export default function Layout({ children, location, route, history, match, ...r
 
   const { type, url, contentType } = initialState?.currentSelectedSys ?? {};
   const userData = localStorage.getItem('userData');
-  const { token } = JSON.parse(userData as string);
+  const { token } = userData ? JSON.parse(userData as string) : { token: '' };
 
-  // if (initialState?.currentSelectedSys) {
-  //   const { type, url, contentType } = initialState.currentSelectedSys;
-  //   if (type == 1 && contentType == 6) {
-  //     const userData = localStorage.getItem('userData');
-  //     const { token } = JSON.parse(userData as string);
-  //     return <iframe id={'bi_iframe'} style={{ width: '100%', height: '100%', border: 'none' }} src={addTokenToUrl(url as string, token)}
-  //     onLoad={() => adjustIframe()}
-
-  //     ></iframe>;
-  //   }
-  // }
-
-  //临时演示处理
+  // 临时演示处理
 
   if (location.pathname == '/platform/costMana') {
     //临时解决未嵌入成本核算,而实现访问的效果
@@ -309,26 +297,6 @@ export default function Layout({ children, location, route, history, match, ...r
     return <>{pageUrl && <iframe id={'bi_iframe'} style={{ width: '100%', height: '100%', border: 'none' }} src={pageUrl} onLoad={() => adjustIframe()}></iframe>};</>;
   }
 
-  // useEffect(() => {
-  //   if (!dataLoaded && initialState) {
-  //     const navData = initialState.navData || [];
-  //     const menuData = initialState.menuData || [];
-
-  //     if (navData.length > 0 || menuData.length > 0) {
-  //       setDataLoaded(true);
-
-  //       const { pathname } = history.location;
-  //       const hasAccess = checkPermission(pathname);
-  //       if (!hasAccess) {
-  //         if (!pathname.includes('/platform')) {
-  //           history.push('/noAccess');
-  //         }
-  //       }
-  //       setLoading(false);
-  //     }
-  //   }
-  // }, [initialState, dataLoaded]);
-
   useEffect(() => {
     return history.listen((location) => {
       if (!isMenuClickRef.current && dataLoaded) {
@@ -350,6 +318,10 @@ export default function Layout({ children, location, route, history, match, ...r
 
   useEffect(() => {
     // 只有从第三方系统切回中台时才处理
+    console.log('isThirdPartySystem',isThirdPartySystem);
+    if(isThirdPartySystem){
+         set_isShowPageMenu(false);
+    }
     if (prevIsThirdPartySystemRef.current && !isThirdPartySystem) {
       // 取当前选中的菜单 key
       if (selectedKeys && selectedKeys.length > 0 && initialState?.menuData) {
@@ -376,7 +348,7 @@ export default function Layout({ children, location, route, history, match, ...r
     }
     prevIsThirdPartySystemRef.current = isThirdPartySystem;
   }, [isThirdPartySystem]);
-  
+    
 
   return (
     <ProLayout

+ 191 - 0
src/pages/platform/setting/aiPromptMana/index.tsx

@@ -0,0 +1,191 @@
+/*
+ * @Author: code4eat awesomedema@gmail.com
+ * @Date: 2025-12-20 10:00:00
+ * @LastEditors: code4eat awesomedema@gmail.com
+ * @LastEditTime: 2025-12-20 10:00:00
+ * @FilePath: /KC-MiddlePlatform/src/pages/platform/setting/aiPromptMana/index.tsx
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ */
+
+import { KCInput } from '@/components/KCInput';
+import KCTable from '@/components/kcTable';
+import { ModalForm, ProFormText, ProFormTextArea } from '@ant-design/pro-form';
+import { ProColumns } from '@ant-design/pro-table';
+import { Popconfirm } from 'antd';
+import { useState } from 'react';
+import { addAiPrompt, getAiPromptList, removeAiPrompt, updateAiPrompt } from './service';
+
+import './style.less';
+
+export default function AiPromptMana() {
+  const [tableDataFilterParams, set_tableDataFilterParams] = useState<{
+    current: number;
+    pageSize: number;
+    code?: string;
+  }>({
+    current: 1,
+    pageSize: 10,
+    code: '',
+  });
+  const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
+  const [reload, set_reload] = useState(false);
+
+  const columns: ProColumns[] = [
+    {
+      title: '编码',
+      dataIndex: 'code',
+    },
+    {
+      title: '提示词',
+      dataIndex: 'prompt',
+      ellipsis: true,
+    },
+    {
+      title: '备注',
+      dataIndex: 'memo',
+      ellipsis: true,
+    },
+    {
+      title: '操作',
+      key: 'option',
+      width: 120,
+      valueType: 'option',
+      render: (_: any, record: any) => {
+        return [
+          <UpDataActBtn key="edit" record={record} type="EDIT" />,
+          <Popconfirm title="是否确认删除?" key="del" onConfirm={() => delTableData(record)}>
+            <a>删除</a>
+          </Popconfirm>,
+        ];
+      },
+    },
+  ];
+
+  const getTableData = async (params: any) => {
+    const { current = 1, pageSize = 10, code } = params || {};
+    const resp = await getAiPromptList({ current, pageSize, code });
+    set_reload(false);
+    if (resp) {
+      return {
+        data: resp.list || [],
+        success: true,
+        total: resp.totalCount,
+        pageSize: resp.pageSize,
+        totalPage: resp.totalPage,
+      };
+    }
+    return {
+      data: [],
+      success: false,
+      total: 0,
+    };
+  };
+
+  const delTableData = async (record: any) => {
+    const resp = await removeAiPrompt([record.id]);
+    if (resp) {
+      set_reload(true);
+    }
+  };
+
+  const updateTable = async (formVal: any, type: 'EDIT' | 'ADD') => {
+    if (type == 'ADD') {
+      const resp = await addAiPrompt(formVal);
+      if (resp) {
+        set_reload(true);
+      }
+    }
+    if (type == 'EDIT') {
+      const resp = await updateAiPrompt(formVal);
+      if (resp) {
+        set_reload(true);
+      }
+    }
+  };
+
+  const UpDataActBtn = ({ record, type }: { record?: any; type: 'EDIT' | 'ADD' }) => {
+    return (
+      <ModalForm
+        title={`${type == 'EDIT' ? '编辑' : '新增'}提示词`}
+        width={520}
+        initialValues={type == 'EDIT' ? { ...record } : {}}
+        trigger={type == 'EDIT' ? <a key="edit">编辑</a> : <span className="add">新增</span>}
+        onFinish={(val) => {
+          return updateTable(type == 'EDIT' ? { ...record, ...val } : val, type);
+        }}
+        colProps={{ span: 24 }}
+        grid
+      >
+        <ProFormText name="code" label="编码:" placeholder="请输入" rules={[{ required: true, message: '编码不能为空!' }]} />
+        <ProFormTextArea
+          name="prompt"
+          label="提示词:"
+          placeholder="请输入"
+          rules={[{ required: true, message: '提示词不能为空!' }]}
+          fieldProps={{ autoSize: { minRows: 6, maxRows: 12 } }}
+        />
+        <ProFormTextArea name="memo" label="备注:" placeholder="请输入" fieldProps={{ autoSize: { minRows: 2, maxRows: 6 } }} />
+      </ModalForm>
+    );
+  };
+
+  const handleTableChange = (pagination: any) => {
+    set_tableDataFilterParams({
+      ...tableDataFilterParams,
+      current: pagination.current,
+      pageSize: pagination.pageSize,
+    });
+  };
+
+  const tableDataSearchHandle = (paramName: string) => {
+    set_tableDataFilterParams({
+      ...tableDataFilterParams,
+      current: 1,
+      [`${paramName}`]: tableDataSearchKeywords,
+    });
+  };
+
+  return (
+    <div className="AiPromptMana">
+      <div className="toolBar">
+        <div className="filter">
+          <div className="filterItem">
+            <span className="label">编码:</span>
+            <KCInput
+              placeholder={'请输入'}
+              style={{ width: 160 }}
+              search
+              allowClear
+              onChange={(e) => {
+                set_tableDataSearchKeywords(e.target.value);
+                if (e.target.value.length == 0) {
+                  set_tableDataFilterParams({
+                    ...tableDataFilterParams,
+                    current: 1,
+                    code: '',
+                  });
+                }
+              }}
+              onSearch={() => tableDataSearchHandle('code')}
+            />
+          </div>
+        </div>
+        <div className="btnGroup">
+          <UpDataActBtn type="ADD" />
+        </div>
+      </div>
+      <div style={{ marginTop: 16 }}>
+        <KCTable
+          columns={columns}
+          scroll={{ y: `calc(100vh - 250px)` }}
+          reload={reload}
+          rowKey="id"
+          newVer
+          params={tableDataFilterParams}
+          request={(params) => getTableData(params)}
+          onChange={handleTableChange}
+        />
+      </div>
+    </div>
+  );
+}

+ 55 - 0
src/pages/platform/setting/aiPromptMana/service.ts

@@ -0,0 +1,55 @@
+/*
+ * @Author: code4eat awesomedema@gmail.com
+ * @Date: 2025-12-20 10:00:00
+ * @LastEditors: code4eat awesomedema@gmail.com
+ * @LastEditTime: 2025-12-20 10:00:00
+ * @FilePath: /KC-MiddlePlatform/src/pages/platform/setting/aiPromptMana/service.ts
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ */
+
+import { request } from 'umi';
+
+export type AiPromptItem = {
+  id: number;
+  code: string;
+  prompt: string;
+  memo?: string;
+  createTime?: string;
+  updateTime?: string;
+};
+
+export type AiPromptListResp = {
+  current: number;
+  list: AiPromptItem[];
+  pageSize: number;
+  totalCount: number;
+  totalPage: number;
+};
+
+export const getAiPromptList = (params: { current: number; pageSize: number; code?: string }) => {
+  return request<AiPromptListResp>('/centerSys/aiPrompt/list', {
+    method: 'GET',
+    params,
+  });
+};
+
+export const addAiPrompt = (data: { code: string; prompt: string; memo?: string }) => {
+  return request('/centerSys/aiPrompt/save', {
+    method: 'POST',
+    data,
+  });
+};
+
+export const updateAiPrompt = (data: { id: number; code: string; prompt: string; memo?: string }) => {
+  return request('/centerSys/aiPrompt/update', {
+    method: 'POST',
+    data,
+  });
+};
+
+export const removeAiPrompt = (ids: number[]) => {
+  return request('/centerSys/aiPrompt/remove', {
+    method: 'POST',
+    data: ids,
+  });
+};

+ 50 - 0
src/pages/platform/setting/aiPromptMana/style.less

@@ -0,0 +1,50 @@
+/*
+ * @Author: code4eat awesomedema@gmail.com
+ * @Date: 2025-12-20 10:00:00
+ * @LastEditors: code4eat awesomedema@gmail.com
+ * @LastEditTime: 2025-12-20 10:00:00
+ * @FilePath: /KC-MiddlePlatform/src/pages/platform/setting/aiPromptMana/style.less
+ * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ */
+
+.AiPromptMana {
+  padding: 16px;
+  background: #FFFFFF;
+  border-radius: 4px;
+
+  .toolBar {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    align-items: center;
+
+    .filter {
+      display: flex;
+      flex-direction: row;
+      justify-content: flex-start;
+      align-items: center;
+
+      .filterItem {
+        display: flex;
+        flex-direction: row;
+        justify-content: center;
+        align-items: center;
+      }
+    }
+
+    .btnGroup {
+      .add {
+        cursor: pointer;
+        display: inline-block;
+        font-size: 14px;
+        font-family: SourceHanSansCN-Normal, SourceHanSansCN;
+        font-weight: 400;
+        color: #FFFFFF;
+        line-height: 24px;
+        padding: 0 14px;
+        background: #3377FF;
+        border-radius: 4px;
+      }
+    }
+  }
+}

+ 82 - 23
src/pages/platform/setting/dataFilling/fillingMana/index.tsx

@@ -49,21 +49,26 @@ export const DATAFILL_DIMENSIONTYPE = [
 ];
 
 type Data = {
-  [key: number]: string;
+  // 键为科室/部门编码,保持字符串类型
+  [key: string]: string;
 };
 
 type LabelValue = {
   label: string;
-  value: number;
+  // value 需保持为字符串编码,避免多选时因 NaN 造成同值冲突
+  value: string;
 };
 
+// 全局简单缓存,避免每次打开不同弹窗都重复请求
+let cachedDeptOptions: LabelValue[] | null = null;
+
 export function transformToLabelValue(data: Data): LabelValue[] {
   const result: LabelValue[] = [];
   for (const key in data) {
     if (data.hasOwnProperty(key)) {
       result.push({
         label: data[key],
-        value: parseInt(key),
+        value: key,
       });
     }
   }
@@ -188,6 +193,20 @@ export default function FillingMana() {
       title: '填报主体',
       ellipsis: true,
       dataIndex: 'typeName',
+      renderText(_, record) {
+        // 根据维度类型从 fillDeptList 提取名称并用逗号拼接
+        const list = Array.isArray(record?.fillDeptList) ? record.fillDeptList : [];
+        const names = list
+          .map((item: any) => {
+            if (record?.dimensionType === 2) {
+              return item?.dimensionTypeName ?? item?.departmentName ?? '';
+            }
+            // 默认按全院/维度类型为 1 时取 departmentName
+            return item?.departmentName ?? item?.dimensionTypeName ?? '';
+          })
+          .filter((s: string) => s);
+        return names.join(',');
+      },
     },
     {
       title: '填报单位',
@@ -354,6 +373,8 @@ export default function FillingMana() {
     const ref = React.createRef<{ save: any }>();
     const [maintype, set_maintype] = useState(undefined);
     const [dimensionType, set_dimensionType] = useState(undefined);
+    // 本地选项缓存,避免每次搜索触发远程过滤
+    const [deptOptions, set_deptOptions] = useState<LabelValue[]>([]);
     const { commitResult } = sideHandle ? sideHandle : {};
 
     useEffect(() => {
@@ -368,6 +389,19 @@ export default function FillingMana() {
       }
     }, [dimensionType]);
 
+    // 弹窗打开时才加载选项,并使用文件级缓存
+    const ensureDeptOptions = async () => {
+      if (deptOptions && deptOptions.length > 0) return;
+      if (cachedDeptOptions && cachedDeptOptions.length > 0) {
+        set_deptOptions(cachedDeptOptions);
+        return;
+      }
+      const depLists = await getCurrentHospAlldeps();
+      const opts = depLists ? transformToLabelValue(depLists) : [];
+      cachedDeptOptions = opts;
+      set_deptOptions(opts);
+    };
+
     return (
       <ModalForm
         title={`${type == 'EDIT' ? '编辑' : '新增'}`}
@@ -377,12 +411,19 @@ export default function FillingMana() {
           type == 'EDIT'
             ? {
                 ...record,
-                fillDeptList: record?.fillDeptList?.map((a: any) => ({ label: a.departmentName, value: Number(a.departmentCode) })) || [],
-                relateDeptList: record?.relateDeptList?.map((a: any) => ({ label: a.departmentName, value: Number(a.departmentCode) })) || [],
+                // 使用字符串编码,避免多选时因 NaN 造成同值冲突
+                fillDeptList: record?.fillDeptList?.map((a: any) => ({ label: a.departmentName, value: a.departmentCode })) || [],
+                relateDeptList: record?.relateDeptList?.map((a: any) => ({ label: a.departmentName, value: a.departmentCode })) || [],
               }
             : { periodType: 1, type: 1, dimensionType: 1 }
         }
-        trigger={type == 'EDIT' ? <a key="edit">编辑</a> : <a className="add">新增</a>}
+        trigger={
+          type == 'EDIT' ? (
+            <a key="edit" onClick={() => ensureDeptOptions()}>编辑</a>
+          ) : (
+            <a className="add" onClick={() => ensureDeptOptions()}>新增</a>
+          )
+        }
         onFinish={(val) => {
           commitResult && commitResult();
           return updateTable(type == 'EDIT' ? { ...val, status: record.status } : { ...val }, type);
@@ -422,24 +463,39 @@ export default function FillingMana() {
                       label="填报主体:"
                       name="fillDeptList"
                       disabled={actType == 'EDIT' && record.limitFlag}
+                      options={deptOptions}
                       fieldProps={
                         type === 2
                           ? {
-                              mode: 'tags',
+                              mode: 'multiple', // 禁止自由录入,避免检索词生成新项
                               maxTagCount: 'responsive',
                               labelInValue: true,
                               size: 'small',
+                              // 开启检索(按名称与编码匹配)
+                              showSearch: true,
+                              optionFilterProp: 'label',
+                              filterOption: (input: string, option: any) => {
+                                const label: string = (option?.label ?? '').toString();
+                                const value: string = (option?.value ?? '').toString();
+                                const kw = input.toLowerCase();
+                                return label.toLowerCase().includes(kw) || value.toLowerCase().includes(kw);
+                              },
+                            }
+                          : {
+                              labelInValue: true,
+                              size: 'small',
+                              // 开启检索(按名称与编码匹配)
+                              showSearch: true,
+                              optionFilterProp: 'label',
+                              filterOption: (input: string, option: any) => {
+                                const label: string = (option?.label ?? '').toString();
+                                const value: string = (option?.value ?? '').toString();
+                                const kw = input.toLowerCase();
+                                return label.toLowerCase().includes(kw) || value.toLowerCase().includes(kw);
+                              },
                             }
-                          : { labelInValue: true, size: 'small' }
                       }
                       rules={[{ required: true, message: '填报主体不能为空!' }]}
-                      request={async () => {
-                        const depLists = await getCurrentHospAlldeps();
-                        if (depLists) {
-                          return transformToLabelValue(depLists);
-                        }
-                        return [];
-                      }}
                     />
 
                     {type != 2 && (
@@ -453,20 +509,23 @@ export default function FillingMana() {
                                   label="相关科室:"
                                   disabled={type == 'EDIT' && record.limitFlag}
                                   name={'relateDeptList'}
+                                  options={deptOptions}
                                   fieldProps={{
-                                    mode: 'tags',
+                                    mode: 'multiple', // 禁止自由录入
                                     maxTagCount: 'responsive',
                                     labelInValue: true,
                                     size: 'small',
+                                    // 开启检索(按名称与编码匹配)
+                                    showSearch: true,
+                                    optionFilterProp: 'label',
+                                    filterOption: (input: string, option: any) => {
+                                      const label: string = (option?.label ?? '').toString();
+                                      const value: string = (option?.value ?? '').toString();
+                                      const kw = input.toLowerCase();
+                                      return label.toLowerCase().includes(kw) || value.toLowerCase().includes(kw);
+                                    },
                                   }}
                                   rules={[{ required: true, message: '相关科室不能为空!' }]}
-                                  request={async () => {
-                                    const depLists = await getCurrentHospAlldeps();
-                                    if (depLists) {
-                                      return transformToLabelValue(depLists);
-                                    }
-                                    return [];
-                                  }}
                                 />
                               )
                             );

+ 5 - 5
src/pages/platform/setting/dataFilling/mineFilling/style.less

@@ -1,8 +1,8 @@
-.kcmpPageContainer {
-  position: relative;
-  margin: 0 !important;
-  padding: 16px;
-}
+// .kcmpPageContainer {
+//   position: relative;
+//   margin: 0 !important;
+//   padding: 16px;
+// }
 
 .MineFilling {
   padding: 16px;

+ 42 - 15
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-05-13 16:51:44
+ * @LastEditTime: 2025-06-24 17:14:16
  * @FilePath: /KC-MiddlePlatform/src/pages/platform/setting/pubDicTypeMana/index.tsx
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */
@@ -17,7 +17,7 @@ import { ProColumns } from '@ant-design/pro-table';
 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';
+import { addData, delData, editData, getDepartmentData, getDepartmentTree, getDepartmentType, getRelaHosp, importDepartmentData } from './service';
 
 import './style.less';
 import { useModel } from 'umi';
@@ -32,6 +32,8 @@ export default function DepartmentMana() {
     current: number;
     pageSize: number;
     name?: string;
+    departType?: string;
+    departName?: string;
   }>({
     current: 1,
     pageSize: 10
@@ -58,7 +60,7 @@ export default function DepartmentMana() {
     {
       title: initialState?.customerType == '2' ? '组织类型' : '科室类型',
       dataIndex: 'type',
-      valueType: 'select',
+      valueType: 'select' as const,
       fieldProps: {
         options: types,
       },
@@ -96,7 +98,7 @@ export default function DepartmentMana() {
       title: '操作',
       key: 'option',
       width: 120,
-      valueType: 'option',
+      valueType: 'option' as const,
       render: (_: any, record: any) => {
         const getEditBtn = () => {
           const { isEdit } = record;
@@ -127,16 +129,33 @@ export default function DepartmentMana() {
   ];
 
   const getTableData: any = async (params: any) => {
-    const resp = await getDepartmentData(params);
     set_reload(false);
-    if (resp) {
-      return {
-        data: resp.list,
-        success: true,
-        total: resp.totalCount,
-        pageSize: resp.pageSize,
-        totalPage: resp.totalPage,
-      };
+    
+    // 根据customerType决定调用哪个接口
+    if (initialState?.customerType == '2') {
+      // 组织模式:调用树形接口
+              const resp = await getDepartmentTree(params);
+        if (resp) {
+          return {
+            data: resp.list, // 直接使用树形数据
+            success: true,
+            total: resp.totalCount,
+            pageSize: resp.pageSize,
+            totalPage: resp.totalPage,
+          };
+        }
+    } else {
+      // 科室模式:调用原有接口
+      const resp = await getDepartmentData(params);
+      if (resp) {
+        return {
+          data: resp.list,
+          success: true,
+          total: resp.totalCount,
+          pageSize: resp.pageSize,
+          totalPage: resp.totalPage,
+        };
+      }
     }
     return [];
   };
@@ -357,8 +376,8 @@ export default function DepartmentMana() {
           <div className="filterItem">
             <span className="label">检索:</span>
             <KCInput
-              placeholder={initialState?.customerType == '2' ? '请输入组织名称!' : '请输入科室名称'}
-              style={{ width: 160 }}
+              placeholder={initialState?.customerType == '2' ? '请输入组织名称' : '请输入科室名称'}
+              style={{ width: 180 }}
               search
               allowClear
               onChange={(e) => {
@@ -390,6 +409,14 @@ export default function DepartmentMana() {
           params={tableDataFilterParams}
           request={(params) => getTableData(params)}
           onChange={handleTableChange}
+          expandable={{
+            defaultExpandAllRows: true, // 默认展开所有行
+            childrenColumnName: 'children', // 指定子数据的字段名
+          }}
+          pagination={{
+            current: tableDataFilterParams.current,
+            pageSize: tableDataFilterParams.pageSize,
+          }}
         />
       </div>
     </div>

+ 28 - 0
src/pages/platform/setting/departmentMana/service.ts

@@ -115,6 +115,34 @@ export const getRelaHosp = () => {
     });
 };
 
+//获取组织树形数据(customerType为2时使用)
+export type TreeDepartmentDataType = {
+    id: number;
+    hospId: string;
+    hospName: string;
+    code: string;
+    name: string;
+    parentId: number;
+    type: string;
+    typeName: string;
+    departmentId: number;
+    isEdit: number;
+    children?: TreeDepartmentDataType[];
+}
+
+export const getDepartmentTree = (params?: any) => {
+    return request<{
+        totalCount: number;
+        pageSize: number;
+        totalPage: number;
+        current: number;
+        list: TreeDepartmentDataType[];
+    }>('/centerSys/sysdepartment/getTree', {
+        method: 'GET',
+        params: { ...params }
+    });
+};
+
 
 
 

+ 87 - 7
src/pages/platform/setting/embeddedDashboard/index.tsx

@@ -1,23 +1,67 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
 import { embedDashboard } from '@superset-ui/embedded-sdk';
+import { getSysParamsByCode } from '@/service';
+import { KcimCenterSysId } from '@/constant';
 import './style.less';
 
+const SUPERSET_ADDRESS_PARAM_CODE = '2029446374776508416';
+
+const normalizeSupersetBaseUrl = (input: string): string | null => {
+  let str = (input || '').toString().trim();
+  if (!str) return null;
+  if (!/^https?:\/\//i.test(str)) {
+    str = `http://${str}`;
+  }
+  try {
+    const u = new URL(str);
+    const basePath = u.pathname && u.pathname !== '/' ? u.pathname.replace(/\/+$/, '') : '';
+    return `${u.origin}${basePath}`;
+  } catch (e) {
+    return null;
+  }
+};
+
 function MyEmbeddedDashboard() {
   // 从 URL 中获取仪表板 ID
   const paths = window.location.pathname.split('/').filter((item) => item);
   const dashboardId = paths[paths.length - 1];
   const host = window.location.hostname;
+  // const host = '120.27.235.181';
+  const [supersetBaseUrl, setSupersetBaseUrl] = useState<string>('');
+
+  useEffect(() => {
+    let cancelled = false;
+    const fallback = `http://${host}:8088`;
+
+    const resolveSupersetBaseUrl = async () => {
+      try {
+        const resp: any = await getSysParamsByCode(KcimCenterSysId, SUPERSET_ADDRESS_PARAM_CODE);
+        const list = Array.isArray(resp) ? resp : Array.isArray(resp?.data) ? resp.data : [];
+        const rawValue = list.find((i: any) => String(i?.code) === SUPERSET_ADDRESS_PARAM_CODE)?.value;
+        const normalized = rawValue ? normalizeSupersetBaseUrl(rawValue) : null;
+        if (!cancelled) setSupersetBaseUrl(normalized || fallback);
+      } catch (e) {
+        if (!cancelled) setSupersetBaseUrl(fallback);
+      }
+    };
+
+    resolveSupersetBaseUrl();
+    return () => {
+      cancelled = true;
+    };
+  }, []);
 
   // 封装获取 Guest Token 的异步函数
   const getGuestToken = async () => {
     try {
+      if (!supersetBaseUrl) return '';
       // 1. 登录,获取 access_token
-      const loginResponse = await fetch(`http://${host}:8088/api/v1/security/login`, {
+      const loginResponse = await fetch(`${supersetBaseUrl}/api/v1/security/login`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
         },
-        body: JSON.stringify({
+        body: JSON.stringify({  
           username: 'kcim',
           password: 'kcim123456',
           provider: 'db', 
@@ -32,7 +76,7 @@ function MyEmbeddedDashboard() {
       }
 
       // 2. 使用 access_token 调用 /api/v1/security/guest_token/ 获取 Guest Token
-      const guestTokenResponse = await fetch(`http://${host}:8088/api/v1/security/guest_token/`, {
+      const guestTokenResponse = await fetch(`${supersetBaseUrl}/api/v1/security/guest_token/`, {
         method: 'POST',
         headers: {
           'Content-Type': 'application/json',
@@ -73,16 +117,52 @@ function MyEmbeddedDashboard() {
 
   useEffect(() => {
     // 当 dashboardId 存在时,嵌入仪表板
-    if (dashboardId) {
+    if (dashboardId && supersetBaseUrl) {
+      // 从 querystring 中解析 urlParams(作为 JSON 字符串传入)
+      const search = window.location.search || '';
+      const usp = new URLSearchParams(search.startsWith('?') ? search.substring(1) : search);
+      const urlParamsStr = usp.get('urlParams') || '';
+      let parsedUrlParams: any = undefined;
+      if (urlParamsStr) {
+        try {
+          parsedUrlParams = JSON.parse(decodeURIComponent(urlParamsStr));
+        } catch (e) {
+          // 解析失败时忽略,避免影响加载
+          parsedUrlParams = undefined;
+        }
+      }
+
+      // 兜底补齐 userId/hospId,读取 localStorage
+      try {
+        const userDataStr = localStorage.getItem('userData');
+        const base: any = {};
+        if (userDataStr) {
+          const user = JSON.parse(userDataStr);
+          if (user?.userId !== undefined) base.userId = user.userId;
+        }
+        const subHop = localStorage.getItem('currentSelectedSubHop');
+        if (subHop) {
+          const parsed = JSON.parse(subHop);
+          if (parsed?.id !== undefined) base.hospId = parsed.id;
+        }
+        if (parsedUrlParams && typeof parsedUrlParams === 'object') {
+          parsedUrlParams = { ...base, ...parsedUrlParams };
+        } else if (Object.keys(base).length > 0) {
+          parsedUrlParams = base;
+        }
+      } catch (e) {}
+
       embedDashboard({
         id: dashboardId,
-        supersetDomain: `http://${host}:8088`, 
+        supersetDomain: supersetBaseUrl,
         mountPoint: document.getElementById('dashboard-container') as HTMLElement,
         dashboardUiConfig: {
           hideTitle: true,
           filters: {
             expanded: true,
           },
+          // 将参数透传给 superset 的 urlParams
+          ...(parsedUrlParams ? { urlParams: parsedUrlParams } : {}),
         },
         // 调用上面封装的 getGuestToken 函数
         fetchGuestToken: async () => {
@@ -92,7 +172,7 @@ function MyEmbeddedDashboard() {
         iframeSandboxExtras: ['allow-top-navigation', 'allow-popups-to-escape-sandbox'],
       });
     }
-  }, [dashboardId]);
+  }, [dashboardId, supersetBaseUrl]);
 
   return (
     <div className="my-embedded-dashboard">

+ 18 - 33
src/pages/platform/setting/hospManage/index.tsx

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2022-01-13 15:22:48
- * @LastEditTime: 2025-05-22 15:53:18
+ * @LastEditTime: 2025-08-11 10:58:44
  * @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
@@ -163,41 +163,23 @@ const DrawerActBtn = ({ record }: { record: any }) => {
           const codes = needItem && needItem.length > 0 && needItem[0].function ? needItem[0].function.map((a: any) => a.code) : [];
 
           const onCheckGroupChange = (checkedValue: CheckboxValueType[]) => {
-            if (checkedValue.length > 0) {
-              const _temp = [...checkBoxCodes];
-              const index = checkBoxCodes.findIndex((item) => item.menuId == record.menuId);
-              const needed = options.filter((item: any) => checkedValue.includes(item.value));
-              const transfered = needed.map((item: any) => ({ name: item.label, code: item.value }));
-
-              if (index >= 0) {
-                //先去除旧的的对象
-                _temp.splice(index, 1);
-              }
-
-              _temp.push({
-                menuId: record.menuId,
-                function: transfered,
-              });
-
-              const thisParents = findParents(hospAllMenuTree, record.menuId);
-
-              const ids = (thisParents as TreeNode[]).map((a) => a.menuId);
+            const needed = options.filter((item: any) => checkedValue.includes(item.value));
+            const transfered = needed.map((item: any) => ({ name: item.label, code: item.value }));
+            const nextCheckBoxCodes = checkBoxCodes.filter((item) => item.menuId != record.menuId);
 
-              set_checkedMenuParentsIds([...checkedMenuParentsIds, ...ids, record.menuId]);
+            nextCheckBoxCodes.push({
+              menuId: record.menuId,
+              function: transfered,
+            });
 
-              set_checkedTableMenuIds([...checkedMenuParentsIds, ...ids, record.menuId]);
+            set_checkBoxCodes(nextCheckBoxCodes);
 
-              set_checkBoxCodes([..._temp]);
-            } else {
-              //取消选择
-              const _temp = checkBoxCodes;
-              const index = checkBoxCodes.findIndex((item) => item.menuId == record.menuId);
-
-              _temp.splice(index, 1);
+            if (checkedValue.length > 0) {
+              const thisParents = findParents(hospAllMenuTree, record.menuId);
+              const ids = (thisParents as TreeNode[]).map((a) => a.menuId);
 
-              // const menuIdsArr = _temp.map((item: any) => item.menuId);
-              // set_checkedTableMenuIds([...menuIdsArr])
-              set_checkBoxCodes([..._temp]);
+              set_checkedMenuParentsIds(Array.from(new Set([...checkedMenuParentsIds, ...ids, record.menuId])));
+              set_checkedTableMenuIds(Array.from(new Set([...checkedTableMenuIds, record.menuId])));
             }
           };
 
@@ -445,7 +427,10 @@ const DrawerActBtn = ({ record }: { record: any }) => {
 
         setAutoExpandParent(true);
         set_currentSelectedTreeNode(node);
-        setExpandedKeys([nodeParent.code]);
+        // 添加空值检查,避免 nodeParent 未定义的情况
+        if (nodeParent?.code) {
+          setExpandedKeys([nodeParent.code]);
+        }
       }
     }
   }, [treeData]);

+ 1 - 1
src/pages/platform/setting/indicatoLagacy/index.tsx

@@ -443,7 +443,7 @@ const IndicatorMana = () => {
               reload={reloadTable}
               rowKey="id"
               newVer
-              params={tableDataFilterParams}
+              params={{ ...tableDataFilterParams, cateId }}
               request={(params) => getData(params)}
               onChange={handleTableChange}
             />

+ 315 - 22
src/pages/platform/setting/indicatorMana/index.tsx

@@ -2,13 +2,13 @@
  * @Author: code4eat awesomedema@gmail.com
  * @Date: 2022-07-12 11:14:21
  * @LastEditors: code4eat awesomedema@gmail.com
- * @LastEditTime: 2025-04-25 17:40:50
+ * @LastEditTime: 2025-08-26 09:38:13
  * @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 React, { useEffect, useMemo, useRef, useState } from 'react';
 import { Dropdown, Empty, Input, Popconfirm, Tabs, Modal, Form, message } from 'antd';
 import { ModalForm, ProFormInstance, ProFormText, ProFormTextArea } from '@ant-design/pro-form';
 
@@ -16,6 +16,7 @@ import DrawerForm from './DrawerForm/drawer';
 import { createFromIconfontCN, DownOutlined } from '@ant-design/icons';
 import {
   addIndicatorManaList,
+  applyStatisticalAnalysisMap,
   applyOnlineReportMap,
   delIndicatorManaList,
   editIndicatorManaList,
@@ -28,13 +29,14 @@ import {
 
 import './style.less';
 import { getIndicatorDictionary, IndicatorDictionaryDataType } from '@/service/dictionary';
-import { useHistory, useLocation, useModel, useParams } from 'umi';
+import { useHistory, useLocation, useModel } from 'umi';
 import { KCIMLeftList } from '../../components/KCIMLeftList';
 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';
+import { TableRequestParamsType } from '@/typings';
 
 const defaultYear = new Date().getFullYear();
 let table_columns: any[] = [];
@@ -61,7 +63,7 @@ const IndicatorMana = () => {
   });
   const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
   const [currentSelectedLeft, set_currentSelectedLeft] = useState<any>(undefined);
-  const [actionType, set_actionType] = useState<'NORMAL' | 'DETAIL' | undefined>('NORMAL');
+  const [actionType, set_actionType] = useState<'NORMAL' | 'DETAIL' | 'ANALYZE' | undefined>('NORMAL');
   const [drawerActype, set_drawerActype] = useState<'ADD' | 'EDIT' | 'DATAMANA' | undefined>(undefined);
   const [tableDataSource, set_tableDataSource] = useState<any[]>([]);
   const [currentYear, set_currentYear] = useState(defaultYear);
@@ -72,10 +74,138 @@ const IndicatorMana = () => {
   const indicatorTableRef = useRef<ActionType>();
   const history: any = useHistory();
   const [currentTab, set_currentTab] = useState('1');
-  const { indicatorType: indicatorUrlType }: { id: string; indicatorType: string } = history.location.query;
+  // 从当前路由解析 indicatorType,确保切换路由参数时能实时更新
 
   const [reportModalVisible, setReportModalVisible] = useState(false);
   const reportForm = useRef<ProFormInstance>();
+  // 报表刷新键,每次保存后自增以强制 iframe 重新加载
+  const [reportReloadKey, set_reportReloadKey] = useState(0);
+
+  // 根据当前路径在菜单树中查找对应菜单节点,解析功能码列表,用于按钮级权限控制
+  const location = useLocation();
+  const { initialState } = useModel('@@initialState');
+  const getMenuNodeByPath = (tree: any[] = [], targetPath: string): any | undefined => {
+    // 1) 标准化:去查询参数、去尾部斜杠
+    const normalize = (p?: string) => {
+      if (!p) return '';
+      const pure = p.split('?')[0];
+      return pure !== '/' ? pure.replace(/\/+$/, '') : pure;
+    };
+    const normalizedTargetPath = normalize(targetPath);
+
+    // 2) 解析当前页面查询参数
+    const parseQuery = (q?: string) => {
+      const out: Record<string, string> = {};
+      if (!q) return out;
+      const s = q.startsWith('?') ? q.substring(1) : q;
+      const usp = new URLSearchParams(s);
+      usp.forEach((v, k) => (out[k] = v));
+      return out;
+    };
+    const currentQuery = parseQuery((location as any).search || window.location.search);
+
+    // 3) 收集同路径候选节点
+    const candidates: any[] = [];
+    const dfs = (nodes: any[]) => {
+      for (const n of nodes) {
+        const nodePath = normalize(n.path);
+        if (nodePath === normalizedTargetPath) {
+          candidates.push(n);
+        }
+        if (n.children && n.children.length > 0) dfs(n.children);
+      }
+    };
+    dfs(tree);
+
+    if (candidates.length === 0) return undefined;
+    if (candidates.length === 1) return candidates[0];
+
+    // 4) 若有多个,用查询参数进行打分匹配(节点自身 path 上的 query 必须被当前页面包含且值相同)
+    const score = (n: any) => {
+      const parts = String(n.path || '').split('?');
+      if (parts.length < 2) return 0;
+      const nodeQuery = parseQuery(parts[1]);
+      let matched = 0;
+      Object.keys(nodeQuery).forEach((k) => {
+        if (currentQuery[k] !== undefined && currentQuery[k] === nodeQuery[k]) matched += 1;
+      });
+      return matched;
+    };
+    let best = candidates[0];
+    let bestScore = score(best);
+    for (let i = 1; i < candidates.length; i++) {
+      const s = score(candidates[i]);
+      if (s > bestScore) {
+        best = candidates[i];
+        bestScore = s;
+      } else if (s === bestScore) {
+        // 平分时,优先带有 function 的
+        const hasFuncBest = Array.isArray(best?.function) && best.function.length > 0;
+        const hasFuncCurr = Array.isArray(candidates[i]?.function) && candidates[i].function.length > 0;
+        if (!hasFuncBest && hasFuncCurr) best = candidates[i];
+      }
+    }
+
+    // 5) 若仍不唯一,尝试根据当前选中的菜单 key 精确命中
+    const selectedKeysStr = localStorage.getItem('selectedKeys');
+    if (selectedKeysStr) {
+      try {
+        const [selectedKey] = JSON.parse(selectedKeysStr) || [];
+        const byKey = candidates.find((n) => String(n.key) === String(selectedKey));
+        if (byKey) return byKey;
+      } catch (e) {}
+    }
+
+    return best;
+  };
+  const canManageReport = useMemo(() => {
+    const node = getMenuNodeByPath(initialState?.menuData || [], location.pathname);
+    const codes: string[] = Array.isArray((node as any)?.function)
+      ? ((node as any).function as any[]).map((i: any) => i?.code).filter(Boolean)
+      : [];
+    return codes.includes('report_manage');
+  
+  }, [initialState?.menuData, location.pathname, (location as any).search]);
+
+  // 读取基础参数:userId、hospId(院区)
+  const getBaseParams = (): { userId?: string | number; hospId?: string | number } => {
+    const userId = (initialState as any)?.userData?.userId;
+    let hospId: any;
+    try {
+      const subHop = localStorage.getItem('currentSelectedSubHop');
+      if (subHop) {
+        const parsed = JSON.parse(subHop);
+        hospId = parsed?.id;
+      }
+    } catch (e) {}
+    return { userId, hospId };
+  };
+
+  const withBaseParams = (obj: any) => {
+    const base = getBaseParams();
+    // 强制覆盖为当前登录/选择的上下文
+    return { ...obj, ...base };
+  };
+
+  // 校验:只需是合法 JSON(userId、hospId 由前台自动拼接,不强制用户填写)
+  const validateParamJson = (_rule: any, value: any) => {
+    const text = (value ?? '').toString().trim();
+    if (text.length === 0) return Promise.resolve();
+    try {
+      JSON.parse(text);
+      return Promise.resolve();
+    } catch (e) {
+      return Promise.reject(new Error('请输入合法的 JSON'));
+    }
+  };
+
+  // 动态解析当前查询参数中的 indicatorType,支持 pathname/search 变化
+  const indicatorUrlType: string = React.useMemo(() => {
+    const search = (location as any)?.search || window.location.search || '';
+    const s = search.startsWith('?') ? search.substring(1) : search;
+    const usp = new URLSearchParams(s);
+    return usp.get('indicatorType') || '';
+  }, [location.search, location.pathname]);
 
   const columns: ProColumns<any>[] = [
     {
@@ -193,7 +323,15 @@ const IndicatorMana = () => {
           {
             key: '2',
             label: (
-              <a key="linka" href={record.indicatorPath} target="_blank">
+              <a
+                key="linka"
+                onClick={() => {
+                  // 打开简化视图:只展示返回的header和“报告管理”按钮
+                  set_currentEditRow(record);
+                  set_actionType('ANALYZE');
+                  set_currentBigTab('1');
+                }}
+              >
                 统计分析
               </a>
             ),
@@ -454,6 +592,8 @@ const IndicatorMana = () => {
   }, [currentTab, currentYear]);
 
   useEffect(() => {
+    // 路由参数变动时重置页面到初始态
+    set_indicatorCateTreeData([]);
     set_cateId(undefined);
     set_actionType(undefined);
     set_drawerActype(undefined);
@@ -462,9 +602,16 @@ const IndicatorMana = () => {
     set_tableColumns([]);
     set_tableDataSource([]);
     set_currentEditRowData(undefined);
+    set_defaultExpandedRowKeys([]);
+    set_tableDataSearchKeywords('');
+    set_currentSelectedLeft(undefined);
+    set_currentTab('1');
+    set_currentYear(defaultYear);
+    setReportModalVisible(false);
+    set_tableDataFilterParams({ current: 1, pageSize: 10, name: '' });
 
     getIndicatorCateTree();
-  }, [location.search]);
+  }, [location.search, location.pathname]);
 
   useEffect(() => {
     if (!drawerVisible) {
@@ -508,26 +655,68 @@ const IndicatorMana = () => {
   // 处理报告表单提交
   const handleReportSubmit = async (values: any) => {
     try {
+      const isAnalyze = actionType === 'ANALYZE';
+      const isDetailOnline = actionType === 'DETAIL' && currentBigTab === '1';
+
+      // 解析并校验 JSON 字符串;后端需要字符串,空则默认 '{}'
+      const rawParamStr = isAnalyze ? values?.analysisReportParam : values?.onlineReportParam;
+      const trimmed = (rawParamStr ?? '').toString().trim();
+      const textToSend = trimmed.length > 0 ? trimmed : '{}';
+      try {
+        JSON.parse(textToSend);
+      } catch (e) {
+        message.error('参数(JSON)格式不合法');
+        return false;
+      }
+
+      // 不在保存到后端时注入 userId/hospId,仅在 Superset 嵌入时动态拼接
+      const finalParamStr = textToSend;
+
+      if (isAnalyze) {
+        const resp = await applyStatisticalAnalysisMap({
+          code: currentEditRow.code,
+          analysisReportCode: values.reportId,
+          analysisRemark: values.remark,
+          analysisReportParam: finalParamStr,
+        });
+        if (resp) {
+          message.success('提交成功');
+          set_currentEditRow((prevRow: any) => ({
+            ...prevRow,
+            analysisRemark: values.remark,
+            analysisReportCode: values.reportId,
+            analysisReportParam: finalParamStr,
+          }));
+          set_reportReloadKey((k) => k + 1);
+          return true;
+        }
+      } else if (isDetailOnline) {
       const resp = await applyOnlineReportMap({
         code: currentEditRow.code,
         onlineReportCode: values.reportId,
-        onlineRemark: values.remark
+          onlineRemark: values.remark,
+          onlineReportParam: finalParamStr,
       });
       if (resp) {
         message.success('提交成功');
         set_currentEditRow((prevRow: any) => ({
           ...prevRow,
-          onlineRemark:values.remark,
-          onlineReportCode: values.reportId
-        }));
-        return true; // 提交成功后关闭弹窗
+            onlineRemark: values.remark,
+            onlineReportCode: values.reportId,
+            onlineReportParam: finalParamStr,
+          }));
+          set_reportReloadKey((k) => k + 1);
+          return true;
+        }
+      } else {
+        message.error('当前上下文不支持报告管理保存');
+        return false;
       }
     } catch (error) {
       console.error('表单验证或提交失败:', error);
-      message.error('提交失败'); // 添加错误提示
-      return false; // 提交失败,保持弹窗打开
+      message.error('提交失败');
+      return false;
     }
-    // 如果 API 调用失败 (resp 为 false),显式返回 false
     message.error('提交失败');
     return false;
   };
@@ -543,8 +732,39 @@ const IndicatorMana = () => {
         onVisibleChange={setReportModalVisible}
         onFinish={handleReportSubmit}
         width={500}
-        initialValues={currentEditRow&&{reportId: currentEditRow.onlineReportCode,
-          remark: currentEditRow.onlineRemark}}
+        initialValues={
+          currentEditRow && (
+            actionType === 'ANALYZE'
+              ? {
+                  reportId: currentEditRow.analysisReportCode,
+                  remark: currentEditRow.analysisRemark,
+                  analysisReportParam: (() => {
+                    try {
+                      const txt = currentEditRow.analysisReportParam;
+                      if (!txt || String(txt).trim().length === 0) return '{}';
+                      const obj = JSON.parse(String(txt));
+                      return JSON.stringify(obj, null, 2);
+                    } catch (e) {
+                      return String(currentEditRow.analysisReportParam || '{}');
+                    }
+                  })(),
+                }
+              : {
+                  reportId: currentEditRow.onlineReportCode,
+                  remark: currentEditRow.onlineRemark,
+                  onlineReportParam: (() => {
+                    try {
+                      const txt = currentEditRow.onlineReportParam;
+                      if (!txt || String(txt).trim().length === 0) return '{}';
+                      const obj = JSON.parse(String(txt));
+                      return JSON.stringify(obj, null, 2);
+                    } catch (e) {
+                      return String(currentEditRow.onlineReportParam || '{}');
+                    }
+                  })(),
+                }
+          )
+        }
         formRef={reportForm}
         modalProps={{ 
           destroyOnClose: true,
@@ -561,6 +781,24 @@ const IndicatorMana = () => {
           placeholder="请输入"
           rules={[{ required: true, message: '请输入关联报告ID' }]}
         />
+        {actionType === 'ANALYZE' && (
+          <ProFormTextArea
+            name="analysisReportParam"
+            label="参数(JSON)"
+            placeholder='请输入合法 JSON,例如 {"a":1}'
+            rules={[{ validator: validateParamJson }]}
+            fieldProps={{ rows: 6 }}
+          />
+        )}
+        {actionType !== 'ANALYZE' && (
+          <ProFormTextArea
+            name="onlineReportParam"
+            label="参数(JSON)"
+            placeholder='请输入合法 JSON,例如 {"a":1}'
+            rules={[{ validator: validateParamJson }]}
+            fieldProps={{ rows: 6 }}
+          />
+        )}
         <ProFormTextArea
           name="remark"
           label="备注说明"
@@ -600,11 +838,27 @@ const IndicatorMana = () => {
               <div className="detail-noAuthRecord-title">暂无内容</div> */}
               <div className='currentBigTab1content_header'>
                 <span>{currentEditRow?.onlineRemark}</span>
-                <div className='btn' onClick={openReportModal}>
-                  <IconFont type='iconpeizhi' /> 报告管理
-                </div>
+                {canManageReport && (
+                  <div className='btn' onClick={openReportModal}>
+                    <IconFont type='iconpeizhi' /> 报告管理
+                  </div>
+                )}
               </div>
-              {(currentEditRow&&currentEditRow.onlineReportCode)&&<FrameComponent link={`/platform/setting/embeddedDashboard/${currentEditRow.onlineReportCode}?noTopbar=true&noMenu=true`} />}
+              {(currentEditRow && currentEditRow.onlineReportCode) && (
+                <FrameComponent
+                  link={`/platform/setting/embeddedDashboard/${currentEditRow.onlineReportCode}?noTopbar=true&noMenu=true&reload=${reportReloadKey}&urlParams=${encodeURIComponent(
+                    (() => { 
+                      try { 
+                        const base = withBaseParams({});
+                        const raw = currentEditRow?.onlineReportParam ? JSON.parse(currentEditRow.onlineReportParam) : {};
+                        return JSON.stringify({ ...base, ...raw }); 
+                      } catch { 
+                        return JSON.stringify(withBaseParams({})); 
+                      } 
+                    })()
+                  )}`}
+                />
+              )}
             </div>
           )}
 
@@ -660,11 +914,50 @@ const IndicatorMana = () => {
           )}
         </div>
       )}
-      {actionType != 'DETAIL' && (
+      {actionType == 'ANALYZE' && (
+        <div className="FillingMana-detail">
+          <div className="FillingMana-detail-header">
+            <div className="FillingMana-detail-header-title">
+              <div className="backBtn" onClick={() => set_currentEditRow(undefined)}>
+                <IconFont style={{ fontSize: 15 }} type={'iconfanhui'} />
+              </div>
+              <span>{currentEditRow?.name}</span>
+              {canManageReport && (
+                <div className='btn' onClick={openReportModal} style={{ marginLeft: 'auto' }}>
+                  <IconFont type='iconpeizhi' /> 报告管理
+                </div>
+              )}
+            </div>
+            <div className="FillingMana-detail-header-title-sub">
+              <span>指标编码:{currentEditRow?.code}</span>
+              <span>指标定义:{currentEditRow?.targetDefinition}</span>
+            </div>
+          </div>
+          <div className="currentBigTab1content">
+            {(currentEditRow && currentEditRow.analysisReportCode) && (
+              <FrameComponent
+                link={`/platform/setting/embeddedDashboard/${currentEditRow.analysisReportCode}?noTopbar=true&noMenu=true&reload=${reportReloadKey}&urlParams=${encodeURIComponent(
+                  (() => { 
+                    try { 
+                      const base = withBaseParams({});
+                      const raw = currentEditRow?.analysisReportParam ? JSON.parse(currentEditRow.analysisReportParam) : {};
+                      return JSON.stringify({ ...base, ...raw }); 
+                    } catch { 
+                      return JSON.stringify(withBaseParams({})); 
+                    } 
+                  })()
+                )}`}
+              />
+            )}
+          </div>
+        </div>
+      )}
+      {(!actionType || actionType === 'NORMAL') && (
         <div className="content">
           <div className="left">
             {/* <TreeDirectory data={indicatorCateTreeData} onSelectChange={(info) => onSelectChangehandle(info)} /> */}
             <KCIMLeftList
+              key={`${location.pathname}${location.search}`}
               searchKey={'name'}
               listType="tree"
               dataSource={indicatorCateTreeData}

+ 12 - 0
src/pages/platform/setting/indicatorMana/style.less

@@ -59,6 +59,18 @@
           height: 20px;
           line-height: 20px;
         }
+
+        // 统计分析视图中的“报告管理”按钮,样式与相关数据视图一致
+        .btn {
+          cursor: pointer;
+          padding: 2px 6px;
+          font-size: 16px;
+          color: #3376fe;
+          border-radius: 4px;
+          text-align: center;
+          border: 1px solid #3376fe;
+          margin-left: auto;
+        }
       }
 
       .FillingMana-detail-header-title-sub {

+ 27 - 4
src/pages/platform/setting/pubDicMana/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-02-18 15:10:18
+ * @LastEditTime: 2025-06-18 09:33:13
  * @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
  */
@@ -10,7 +10,7 @@
 import { KCInput } from '@/components/KCInput';
 import KCTable from '@/components/kcTable';
 import { getAllHosp } from '@/service/hospList';
-import { ModalForm, ProFormDigit, ProFormRadio, ProFormSelect, ProFormText, ProFormTextArea } from '@ant-design/pro-form';
+import { ModalForm, ProFormDigit, ProFormRadio, ProFormSelect, ProFormText, ProFormTextArea, ProFormSwitch } from '@ant-design/pro-form';
 import { ProColumns } from '@ant-design/pro-table';
 import { Input, message, Modal, Popconfirm, TreeProps } from 'antd';
 import { useEffect, useState } from 'react';
@@ -43,6 +43,8 @@ const PubDicMana = () => {
     current: number;
     pageSize: number;
     name?: string;
+    typeName?: string;
+    dictName?: string;
   }>({
     current: 1,
     pageSize: 10
@@ -99,6 +101,13 @@ const PubDicMana = () => {
       width: 120,
       valueType: 'option',
       render: (_: any, record: any) => {
+        if (record.isEdit === 1) {
+          const disabledBase = { color: '#999', cursor: 'not-allowed' } as React.CSSProperties;
+          return [
+            <span key="edit-disabled" style={{ ...disabledBase }}>编辑</span>,
+            <span key="del-disabled" style={{ ...disabledBase }}>删除</span>,
+          ];
+        }
         return [
           <UpDataActBtn key={'act'} record={record} type="EDIT" />,
           <Popconfirm title="是否确认删除?" key="del" onConfirm={() => delTableData(record)}>
@@ -180,10 +189,18 @@ const PubDicMana = () => {
       <ModalForm
         title={`${type == 'EDIT' ? '编辑' : '新增'}公用字典(${currentSelectedTreeNode?.dictName})`}
         width={352}
-        initialValues={type == 'EDIT' ? { ...record } : {}}
+        initialValues={type == 'EDIT' ? { ...record, isEdit: record.isEdit == 1 ? true : false } : { isEdit: false }}
         trigger={type == 'EDIT' ? <a key="edit">编辑</a> : <span className="add">新增</span>}
         onFinish={(val) => {
-          return updateTable(type == 'EDIT' ? (pageType == 1 ? { ...val, dictDataId: record.dictDataId } : { ...val, id: record.id }) : val, type);
+          const submitVal = { ...val, isEdit: val.isEdit ? 1 : 0 };
+          return updateTable(
+            type == 'EDIT'
+              ? (pageType == 1
+                  ? { ...submitVal, dictDataId: record.dictDataId }
+                  : { ...submitVal, id: record.id })
+              : submitVal,
+            type
+          );
         }}
       >
         <ProFormText name="name" label={tableFileNames.length == 7 ? `${tableFileNames[0]}:` : '名称:'} placeholder="请输入" rules={[{ required: true, message: '名称不能为空!' }]} />
@@ -215,6 +232,10 @@ const PubDicMana = () => {
             <ProFormText name="expandTwo" label={tableFileNames.length == 7 ? `${tableFileNames[6]}:` : '扩展二:'} placeholder="请输入" />
           </>
         )}
+        <div style={{ display: 'flex', alignItems: 'center', marginBottom: 12 }}>
+          <span style={{ marginRight: 4, color: 'rgba(0,0,0,0.85)', fontSize: 14, fontWeight: 400 }}>不可编辑:</span>
+          <ProFormSwitch noStyle fieldProps={{ size: 'small' }} name="isEdit" />
+        </div>
       </ModalForm>
     );
   };
@@ -399,6 +420,7 @@ const PubDicMana = () => {
                   ) : (
                     <span className="strTitle">{strTitle}</span>
                   );
+                const showDot = nodeData.isEdit === 1 && !nodeData.children;
                 return (
                   <div
                     style={{
@@ -416,6 +438,7 @@ const PubDicMana = () => {
                     }}
                   >
                     {title}
+                    {showDot && <span style={{ width: 6, height: 6, borderRadius: '50%', background: '#EC0000', marginLeft: 4 }} />}
                   </div>
                 );
               }}

+ 34 - 9
src/pages/platform/setting/pubDicTypeMana/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-02-18 15:10:34
+ * @LastEditTime: 2025-06-18 16:33:15
  * @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
  */
@@ -10,9 +10,9 @@
 import { KCInput } from '@/components/KCInput';
 import KCTable from '@/components/kcTable';
 
-import { ModalForm, ProFormSelect, ProFormText, ProFormTextArea } from '@ant-design/pro-form';
+import { ModalForm, ProFormSelect, ProFormText, ProFormTextArea, ProFormSwitch, ProForm } from '@ant-design/pro-form';
 import { ProColumns } from '@ant-design/pro-table';
-import { Input, message, Modal, Popconfirm, Switch, TreeProps } from 'antd';
+import { Input, message, Modal, Popconfirm, Switch, TreeProps, Form } from 'antd';
 import React, { useEffect, useState } from 'react';
 import { addData, delData, dicOpenStatHandle, editData, getData, getTreeData } from './service';
 
@@ -35,6 +35,8 @@ export default function PubDicTypeMana() {
     current: number;
     pageSize: number;
     name?: string;
+    typeName?: string;
+    systemId?: string;
   }>({
     current: 1,
     pageSize: 10
@@ -79,8 +81,15 @@ export default function PubDicTypeMana() {
       width: 120,
       valueType: 'option',
       render: (_: any, record: any) => {
+        if (record.isEdit === 1) {
+          const disabledBase = { color: '#999', cursor: 'not-allowed' } as React.CSSProperties;
+          return [
+            <span key="edit-disabled" style={{ ...disabledBase }}>编辑</span>,
+            <span key="del-disabled" style={disabledBase}>删除</span>,
+          ];
+        }
         return [
-          <UpDataActBtn key={'act'} record={record} type="EDIT" />,
+          <UpDataActBtn key="act" record={record} type="EDIT" />,
           <Popconfirm title="是否确认删除?" key="del" onConfirm={() => delTableData(record)}>
             <a>删除</a>
           </Popconfirm>,
@@ -118,7 +127,8 @@ export default function PubDicTypeMana() {
     }
   };
 
-  const updateTable = async (formVal: any, type: 'EDIT' | 'ADD') => {
+  const updateTable = async (formVal: any, type: 'EDIT' | 'ADD', record?: any) => {
+    const isEdit = formVal.isEdit ? 1 : 0;
     if (type == 'ADD') {
       const resp = await addData({
         dictName: formVal.dictName,
@@ -126,13 +136,14 @@ export default function PubDicTypeMana() {
         remark: formVal.remark,
         topic: formVal.topic ? formVal.topic : null,
         systemId: currentSelectedTreeNode.code,
+        isEdit,
       });
       if (resp) {
         set_reload(true);
       }
     }
     if (type == 'EDIT') {
-      const { dictId } = currentEdit;
+      const { dictId } = record || currentEdit;
       const resp = await editData({
         dictId,
         dictName: formVal.dictName,
@@ -140,6 +151,7 @@ export default function PubDicTypeMana() {
         remark: formVal.remark,
         topic: formVal.topic ? formVal.topic : null,
         systemId: currentSelectedTreeNode.code,
+        isEdit,
       });
       if (resp) {
         set_reload(true);
@@ -152,10 +164,10 @@ export default function PubDicTypeMana() {
       <ModalForm
         title={`${type == 'EDIT' ? '编辑' : '新增'}公用字典类型`}
         width={352}
-        initialValues={type == 'EDIT' ? { ...record } : {}}
+        initialValues={type === 'EDIT' ? { ...record, isEdit: record.isEdit == 1 ? true : false } : { isEdit: false }}
         trigger={
           type == 'EDIT' ? (
-            <a key="edit" onClick={() => set_currentEdit(record)}>
+            <a key="edit">
               编辑
             </a>
           ) : (
@@ -163,7 +175,7 @@ export default function PubDicTypeMana() {
           )
         }
         onFinish={(val) => {
-          return updateTable(val, type);
+          return updateTable(val, type, record);
         }}
       >
         {/* <ProFormSelect
@@ -193,6 +205,19 @@ export default function PubDicTypeMana() {
         <ProFormText name="dictType" label="类型代码:" placeholder="请输入" rules={[{ required: true, message: '类型代码不能为空!' }]} />
         <ProFormTextArea name="topic" label="标题(|线分割):" placeholder="请输入" />
         <ProFormTextArea name="remark" label="说明:" placeholder="请输入" />
+        <div style={{ display: 'flex', alignItems: 'center', }}>
+          <span
+            style={{
+              marginRight: 4,
+              color: 'rgba(0,0,0,0.85)',
+              fontSize: 14,
+              fontWeight: 400,
+            }}
+          >
+            不可编辑:
+          </span>
+          <ProFormSwitch noStyle fieldProps={{ size: 'small' }} name="isEdit" />
+        </div>
       </ModalForm>
     );
   };

+ 2 - 0
src/pages/platform/setting/pubDicTypeMana/service.ts

@@ -35,6 +35,7 @@ export type AddTableDataType = {
   remark: string;
   systemId: string;
   topic: string;
+  isEdit?: number;
 };
 export const addData = (data: AddTableDataType) => {
   return request('/centerSys/sysdictdata/addSystemDict', {
@@ -63,6 +64,7 @@ export type EditTableDataType = {
   dictType: string;
   remark: string;
   topic: string;
+  isEdit?: number;
 };
 
 export const editData = (data: EditTableDataType) => {

+ 132 - 90
src/pages/platform/setting/roleManage/index.tsx

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2022-01-13 15:22:48
- * @LastEditTime: 2025-05-21 17:50:49
+ * @LastEditTime: 2025-06-16 18:07:15
  * @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
@@ -9,7 +9,7 @@
 
 import { FC, Key, useEffect, useState } from 'react';
 import { roleManageModelState, ConnectProps, Loading, connect, useModel } from 'umi';
-import { Checkbox, Divider, Dropdown, Input, Popconfirm, Switch, Transfer, TreeProps, Modal } from 'antd';
+import { Checkbox, Divider, Dropdown, Input, Popconfirm, Switch, Transfer, TreeProps, Modal, Tooltip } from 'antd';
 import KCTable from '@/components/kcTable';
 import type { ProColumns } from '@ant-design/pro-table';
 import { getAllRoles, getRoleHasBindMenus, getRoleHasBindUsers, getRolePermRelaMenu, initRoleFunc, initRoleReadOnlyFunc, saveRoleRelaApiPerm } from '@/service/role';
@@ -58,7 +58,7 @@ const SearchIcon = createFromIconfontCN({
 
 const selectSystem: { systemId: string; type: number }[] = [];
 
-const DrawerActBtn = ({ record }: { record: any }) => {
+const DrawerActBtn = ({ record, dispatch }: { record: any; dispatch: any }) => {
   const { initialState, setInitialState } = useModel('@@initialState');
   const [treeData, set_treeData] = useState<getTreeDataRespType[]>([]);
   const [currentSelectedTreeNode, set_currentSelectedTreeNode] = useState<any | undefined>(undefined);
@@ -140,47 +140,23 @@ const DrawerActBtn = ({ record }: { record: any }) => {
           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);
-
-              const needed = options.filter((item: any) => checkedValue.includes(item.value));
-              const transfered = needed.map((item: any) => ({ name: item.label, code: item.value }));
+            const needed = options.filter((item: any) => checkedValue.includes(item.value));
+            const transfered = needed.map((item: any) => ({ name: item.label, code: item.value }));
+            const nextCheckBoxCodes = checkBoxCodes.filter((item) => item.menuId != record.menuId);
 
-              if (index >= 0) {
-                _temp.splice(index, 1);
-              }
+            nextCheckBoxCodes.push({
+              menuId: record.menuId,
+              function: transfered,
+            });
 
-              _temp.push({
-                menuId: record.menuId,
-                function: transfered,
-              });
+            set_checkBoxCodes(nextCheckBoxCodes);
 
-              // 2. 获取并添加父级菜单ID
+            if (checkedValue.length > 0) {
               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);
-              
-              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));
-                }
-              }
+              set_checkedMenuParentsIds(Array.from(new Set([...checkedMenuParentsIds, record.menuId, ...parentsIds])));
+              set_checkedTableMenuIds(Array.from(new Set([...checkedTableMenuIds, record.menuId])));
             }
           };
 
@@ -429,6 +405,12 @@ const DrawerActBtn = ({ record }: { record: any }) => {
         set_checkBoxCodes([]);
         set_checkedTableMenuIds([]);
         await setInitCheckData();
+        
+        // 刷新主表格数据
+        dispatch &&
+          dispatch({
+            type: 'roleManageModel/reloadTable',
+          });
       }
     } catch (error) {
       console.error('保存角色权限失败:', error);
@@ -493,13 +475,14 @@ const DrawerActBtn = ({ record }: { record: any }) => {
         const [node, nodeParent] = getDeepestTreeData(treeData[0], 'children');
         setAutoExpandParent(true);
         set_currentSelectedTreeNode(node);
-        setExpandedKeys([nodeParent.code]);
+        // 添加空值检查以避免 nodeParent 未定义的情况
+        if (nodeParent) {
+          setExpandedKeys([nodeParent.code]);
+        }
       }
     }
   }, [treeData]);
 
-  useEffect(() => {}, [checkedMenuParentsIds]);
-
   useEffect(() => {
     if (drawerVisible) {
       getTreeReqFunc(record.hospId);
@@ -769,7 +752,6 @@ const RoleManage: FC<PageProps> = ({ roleManageModel: state, dispatch }) => {
     roleName: '',
   });
   const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
-  const [reload, set_reload] = useState(false);
 
   const columns: ProColumns<TableListItem>[] = [
     {
@@ -835,53 +817,113 @@ const RoleManage: FC<PageProps> = ({ roleManageModel: state, dispatch }) => {
       width: 200,
       key: 'option',
       valueType: 'option',
-      render: (text, record) => [
-        <a key="link4" onClick={() => editUserBind(record)}>
-          用户
-        </a>,
-        <Divider key="9" type="vertical" style={{ margin: '0 1px' }} />,
-        <DrawerActBtn key="link7" record={record} />,
-        <Divider key="3" type="vertical" style={{ margin: '0 1px' }} />,
-        <Popconfirm
-          title="是否确定删除?"
-          onConfirm={() => delHandle(record)}
-          // onCancel={cancel}
-          okText="确定"
-          cancelText="取消"
-          key="link2"
-        >
-          <a>删除</a>
-        </Popconfirm>,
-        <Divider key="10" type="vertical" style={{ margin: '0 1px' }} />,
-        <Dropdown
-          key="4"
-          menu={{
-            items: [
-              {
-                key: '5',
-                label: (
-                  <a key="link" onClick={() => editHandle(record)}>
-                    编辑
-                  </a>
-                ),
-              },
-              {
-                key: '2',
-                label: (
-                  <a key="link4" onClick={() => initRoleData('func', record)}>
-                    初始化权限
-                  </a>
-                ),
-              },
-              //{ key: '6', label: <a key="link6" onClick={() => initRoleData('read',record)}>初始化只读</a> },
-            ],
-          }}
-        >
-          <a>
-            更多 <DownOutlined />
-          </a>
-        </Dropdown>,
-      ],
+      render: (text, record) => {
+        const { isEdit} = record;
+        if (isEdit) {
+          return [
+            <Tooltip key="link4-tooltip" title="第三方角色不可编辑用户">
+              <span style={{ cursor: 'not-allowed', color: 'rgb(176 181 189)' }}>用户</span>
+            </Tooltip>,
+            <Divider key="9" type="vertical" style={{ margin: '0 1px' }} />,
+            <DrawerActBtn key="link7" record={record} dispatch={dispatch} />,
+            <Divider key="3" type="vertical" style={{ margin: '0 1px' }} />,
+            <Tooltip key="link2-tooltip" title="第三方角色不可删除">
+              <span style={{ cursor: 'not-allowed', color: 'rgb(176 181 189)' }}>删除</span>
+            </Tooltip>,
+            <Divider key="10" type="vertical" style={{ margin: '0 1px' }} />,
+            <Dropdown
+              key="4"
+              menu={{
+                items: [
+                  {
+                    key: '5',
+                    label: isEdit ? (
+                      <Tooltip title="第三方角色不可编辑">
+                        <span style={{ cursor: 'not-allowed', color: 'rgb(176 181 189)' }}>编辑</span>
+                      </Tooltip>
+                    ) : (
+                      <a key="link" onClick={() => editHandle(record)}>
+                        编辑
+                      </a>
+                    ),
+                  },
+                  {
+                    key: '2',
+                    label: isEdit ? (
+                      <Tooltip title="第三方角色不可初始化权限">
+                        <span style={{ cursor: 'not-allowed', color: 'rgb(176 181 189)' }}>初始化权限</span>
+                      </Tooltip>
+                    ) : (
+                      <a key="link4" onClick={() => initRoleData('func', record)}>
+                        初始化权限
+                      </a>
+                    ),
+                  },
+                ],
+              }}
+            >
+              <a>
+                更多 <DownOutlined />
+              </a>
+            </Dropdown>,
+          ];
+        } else {
+          return [
+            <a key="link4" onClick={() => editUserBind(record)}>
+              用户
+            </a>,
+            <Divider key="9" type="vertical" style={{ margin: '0 1px' }} />,
+            <DrawerActBtn key="link7" record={record} dispatch={dispatch} />,
+            <Divider key="3" type="vertical" style={{ margin: '0 1px' }} />,
+            <Popconfirm
+              title="是否确定删除?"
+              onConfirm={() => delHandle(record)}
+              // onCancel={cancel}
+              okText="确定"
+              cancelText="取消"
+              key="link2"
+            >
+              <a>删除</a>
+            </Popconfirm>,
+            <Divider key="10" type="vertical" style={{ margin: '0 1px' }} />,
+            <Dropdown
+              key="4"
+              menu={{
+                items: [
+                  {
+                    key: '5',
+                    label: isEdit ? (
+                      <Tooltip title="第三方角色不可编辑">
+                        <span style={{ cursor: 'not-allowed', color: 'rgb(176 181 189)' }}>编辑</span>
+                      </Tooltip>
+                    ) : (
+                      <a key="link" onClick={() => editHandle(record)}>
+                        编辑
+                      </a>
+                    ),
+                  },
+                  {
+                    key: '2',
+                    label: isEdit ? (
+                      <Tooltip title="第三方角色不可初始化权限">
+                        <span style={{ cursor: 'not-allowed', color: 'rgb(176 181 189)' }}>初始化权限</span>
+                      </Tooltip>
+                    ) : (
+                      <a key="link4" onClick={() => initRoleData('func', record)}>
+                        初始化权限
+                      </a>
+                    ),
+                  },
+                ],
+              }}
+            >
+              <a>
+                更多 <DownOutlined />
+              </a>
+            </Dropdown>,
+          ];
+        }
+      },
     },
   ];
 
@@ -1045,7 +1087,7 @@ const RoleManage: FC<PageProps> = ({ roleManageModel: state, dispatch }) => {
         <KCTable
           columns={columns}
           scroll={{ y: `calc(100vh - 250px)` }}
-          reload={reload}
+          reload={reloadTable}
           rowKey="id"
           newVer
           params={tableDataFilterParams}

+ 3 - 2
src/pages/platform/setting/serviceEvaluate/index.tsx

@@ -727,8 +727,9 @@ const ServiceEvaluatePage: React.FC = () => {
                         onClick={async (e)=>{
                           e.stopPropagation();
                         set_serviceBindSystemId(String(nodeData.id));
-                        // 打开前重置已选,避免上次选择残留
-                        set_serviceSelected([]);
+                        // 回显当前系统已有的服务
+                        const existingServices = (nodeData.children || []).map((child: any) => String(child.code));
+                        set_serviceSelected(existingServices);
                         await fetchServiceDict();
                         set_serviceModalOpen(true);
                         }}

+ 1 - 0
src/pages/platform/setting/static/index.tsx

@@ -13,6 +13,7 @@ import { Skeleton } from 'antd';
 import { useModel } from 'umi';
 
 export default () => {
+  
   const { initialState, setInitialState } = useModel('@@initialState');
   const [specialPageUrl, setspecialPageUrl] = useState<string | undefined>(undefined);
   const [loading, setloading] = useState(false);

+ 127 - 4
src/pages/platform/setting/userManage/index.tsx

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2022-01-11 09:43:18
- * @LastEditTime: 2025-05-13 15:32:58
+ * @LastEditTime: 2025-06-16 18:12:14
  * @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
@@ -9,7 +9,7 @@
 
 import { FC, useEffect, useState } from 'react';
 import { userManageModelState, ConnectProps, Loading, connect, useModel } from 'umi';
-import { Button, Divider, Popconfirm, Popover, Tooltip } from 'antd';
+import { Button, Divider, Popconfirm, Popover, Tooltip, Modal, message, Upload } from 'antd';
 import KCTable from '@/components/kcTable';
 import type { ProColumns } from '@ant-design/pro-table';
 import { getUserRelaSeletData, getUsers, UserRelaSeletDataListType, UserRelaSeletDataType } from '@/service/user';
@@ -23,6 +23,7 @@ import { RcFile } from 'antd/lib/upload/interface';
 import { KCInput } from '@/components/KCInput';
 import FormItem from 'antd/es/form/FormItem';
 import { getDepartmentData } from '../departmentMana/service';
+import { UploadOutlined } from '@ant-design/icons';
 
 export enum TableActType {
   NOACT,
@@ -49,6 +50,12 @@ const UserManage: FC<PageProps> = ({ userManageModel: state, dispatch }) => {
   const [tableDataSearchKeywords, set_tableDataSearchKeywords] = useState<string>('');
 
   const [dirData, set_dirData] = useState<UserRelaSeletDataType[]>([]);
+  const [uploadAvatarModalVisible, set_uploadAvatarModalVisible] = useState(false);
+  const [currentUser, set_currentUser] = useState<any>(null);
+
+  // 头像预览和上传状态
+  const [avatarUrl, set_avatarUrl] = useState<string | undefined>(undefined);
+  const [uploading, set_uploading] = useState(false);
 
   const columns: ProColumns<TableListItem>[] = [
     {
@@ -114,7 +121,11 @@ const UserManage: FC<PageProps> = ({ userManageModel: state, dispatch }) => {
       title: '操作',
       width: 180,
       key: 'option',
+      dataIndex: 'option',
       valueType: 'option',
+      onCell: (record) => ({
+        className: record.isEdit === 1 ? 'has-upload' : '',
+      }),
       render: (text, record) => {
         const getEditBtn = () => {
           const { isEdit } = record;
@@ -127,7 +138,6 @@ const UserManage: FC<PageProps> = ({ userManageModel: state, dispatch }) => {
               <Popconfirm
                 title="是否确定删除?"
                 onConfirm={() => delHandle(record)}
-                // onCancel={cancel}
                 okText="确定"
                 cancelText="取消"
                 key="link2"
@@ -147,13 +157,25 @@ const UserManage: FC<PageProps> = ({ userManageModel: state, dispatch }) => {
             ];
           }
         };
-        return [
+
+        const operationButtons = [
           ...getEditBtn(),
           <Divider key="2" type="vertical" style={{ margin: '0 3px' }} />,
           <a key="link3" onClick={() => resetUserPaswdHandle(record)}>
             重置密码
           </a>,
         ];
+
+        if (record.isEdit === 1) {
+          operationButtons.push(
+            <Divider key="3" type="vertical" style={{ margin: '0 3px' }} />,
+            <a key="link4" onClick={() => uploadAvatarHandle(record)}>
+              上传头像
+            </a>
+          );
+        }
+
+        return operationButtons;
       },
     },
   ];
@@ -319,6 +341,97 @@ const UserManage: FC<PageProps> = ({ userManageModel: state, dispatch }) => {
     };
   };
 
+  const uploadAvatarHandle = (record: TableListItem) => {
+    set_currentUser(record);
+    set_avatarUrl(record.avatarUrl || undefined);
+    set_uploadAvatarModalVisible(true);
+  };
+
+  // 上传头像弹窗内容
+  const uploadAvatarModalContent = (
+    <div style={{ textAlign: 'center', padding: 24 }}>
+      <div style={{ position: 'relative', display: 'inline-block', marginBottom: 16 }}>
+        {avatarUrl ? (
+          <img
+            src={avatarUrl}
+            alt="avatar"
+            style={{ width: 96, height: 96, borderRadius: '50%', objectFit: 'cover', border: '1px solid #eee' }}
+          />
+        ) : (
+          <div
+            style={{
+              width: 96,
+              height: 96,
+              borderRadius: '50%',
+              background: '#f2f2f2',
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'center',
+              color: '#bbb',
+              fontSize: 14,
+              border: '1px solid #eee',
+            }}
+          >
+            <img src="/images/avatar.png" alt="avatar" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
+          </div>
+        )}
+        <Upload
+          name="file"
+          showUploadList={false}
+          accept=".jpg,.jpeg,.png"
+          action="/gateway/centerSys/user/updateAvatar"
+          headers={{
+            token: JSON.parse(localStorage.getItem('userData') || '{}').token || ''
+          }}
+          data={{ userId: currentUser?.id }}
+          onChange={info => {
+            if (info.file.status === 'done') {
+              set_uploading(false);
+              if (info.file.response && info.file.response.status === 200) {
+                message.success('上传成功');
+                set_avatarUrl(info.file.response.data?.url || undefined);
+                set_uploadAvatarModalVisible(false);
+                dispatch && dispatch({ type: 'userManageModel/reloadTable', payload: true });
+              } else {
+                message.error(info.file.response?.msg || '上传失败');
+              }
+            } else if (info.file.status === 'error') {
+              set_uploading(false);
+              message.error('上传失败');
+            } else if (info.file.status === 'uploading') {
+              set_uploading(true);
+            }
+          }}
+        >
+          <div
+            style={{
+              position: 'absolute',
+              left: 0,
+              top: 0,
+              width: 96,
+              height: 96,
+              borderRadius: '50%',
+              background: 'rgba(0,0,0,0.4)',
+              color: '#fff',
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'center',
+              opacity: 0,
+              cursor: 'pointer',
+              transition: 'opacity 0.2s',
+            }}
+            className="avatar-upload-mask"
+            onMouseEnter={e => (e.currentTarget.style.opacity = '1')}
+            onMouseLeave={e => (e.currentTarget.style.opacity = '0')}
+          >
+            {uploading ? '上传中...' : '更换头像'}
+          </div>
+        </Upload>
+      </div>
+      <div style={{ color: '#888', fontSize: 12 }}>支持jpg、jpeg、png格式,建议尺寸1:1</div>
+    </div>
+  );
+
   useEffect(() => {
     getDirecData();
   }, []);
@@ -329,6 +442,16 @@ const UserManage: FC<PageProps> = ({ userManageModel: state, dispatch }) => {
     <div className="UserManage">
       <ActModal {...state} dispatch={dispatch} />
 
+      <Modal
+        title="上传头像"
+        open={uploadAvatarModalVisible}
+        onCancel={() => set_uploadAvatarModalVisible(false)}
+        footer={null}
+        destroyOnClose
+      >
+        {uploadAvatarModalContent}
+      </Modal>
+
       <div className="toolBar">
         <div className="filter">
           <div className="filterItem" style={{ marginRight: 16 }}>

+ 13 - 14
src/pages/platform/setting/userManage/modal.tsx

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2022-01-12 17:11:11
- * @LastEditTime: 2025-02-24 10:52:46
+ * @LastEditTime: 2025-06-16 17:48:09
  * @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/userManage/modal.tsx
@@ -51,28 +51,27 @@ const ActModal: React.FC<ActModalProps> = ({ dispatch, isShowModal, tableAct, cu
     name: 'file',
     maxCount: 1,
     showUploadList: false,
-    onChange: async (info: { file: any; fileList: any }) => {
-      setLoadAvatar(true);
-
-      if (info.file.status !== 'uploading') {
+    customRequest: async ({ file, onSuccess, onError }) => {
+      try {
         const form = new FormData();
-        form.append('file', info.file.originFileObj);
+        form.append('file', file);
         const resp = await uploadAvatar(form);
         if (resp) {
           setAvatarUrl(resp);
+          onSuccess?.('ok');
         }
-        // const reader = new FileReader();
-        // reader.readAsDataURL(info.file.originFileObj); //读取图像文件 result 为 DataURL, DataURL 可直接 赋值给 img.src
-        // reader.onload = function (event) {
-        //   if (event.target && typeof event.target.result == 'string') {
-        //     setAvatarUrl(event.target.result); //base64
-        //   }
-        // };
+      } catch (error) {
+        const uploadError = new Error('Upload failed') as any;
+        onError?.(uploadError);
       }
+    },
+    onChange: async (info: { file: any; fileList: any }) => {
+      setLoadAvatar(true);
       if (info.file.status === 'done') {
         setLoadAvatar(false);
       } else if (info.file.status === 'error') {
         message.error(`${info.file.name} 上传失败!`);
+        setLoadAvatar(false);
       }
     },
   };
@@ -217,7 +216,7 @@ const ActModal: React.FC<ActModalProps> = ({ dispatch, isShowModal, tableAct, cu
                   showUploadList={false}
                   {...props}
                 >
-                  {avatarUrl ? <img src={avatarUrl} alt="avatar" style={{ width: '100%' }} /> : uploadButton}
+                  {avatarUrl ? <img src={avatarUrl} alt="avatar" style={{ width: '100%' }} /> : <img src="/images/avatar.png" alt="avatar" style={{ width: '100%' }} />}
                 </Upload>
               </div>
 

+ 11 - 0
src/pages/platform/setting/userManage/style.less

@@ -65,6 +65,17 @@
     }
 
   }
+
+  :global {
+    .ant-table-cell {
+      &[data-column-key='option'] {
+        width: 180px !important;
+        &.has-upload {
+          width: 240px !important;
+        }
+      }
+    }
+  }
 }
 
 .ProfileModal {

+ 1 - 1
src/service/hospList.ts

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2022-01-13 09:15:59
- * @LastEditTime: 2023-09-14 14:00:11
+ * @LastEditTime: 2025-08-26 15:51:18
  * @LastEditors: code4eat awesomedema@gmail.com
  * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  * @FilePath: /KC-MiddlePlatform/src/service/hospList.ts

+ 47 - 1
src/service/index.ts

@@ -2,7 +2,7 @@
  * @Author: code4eat awesomedema@gmail.com
  * @Date: 2022-06-27 15:43:25
  * @LastEditors: code4eat awesomedema@gmail.com
- * @LastEditTime: 2025-01-09 10:36:34
+ * @LastEditTime: 2025-12-15 13:47:57
  * @FilePath: /KC-MiddlePlatform/src/service/index.ts
  * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  */
@@ -71,6 +71,17 @@ export const getSysParamsByCode = async (systemId: string, parameterCode?: strin
   });
 };
 
+// 根据系统id与参数code获取默认参数(用于兜底hospSign)
+export const getDefaultParam = async (systemId: string, paramCode: string) => {
+  return request<{ code: string; name: string; value: string }>(
+    '/centerSys/parameter/getDefaultParam',
+    {
+      method: 'GET',
+      params: { systemId, paramCode },
+    },
+  );
+};
+
 //校验是否具有应用使用权限
 
 export const getAppAccess = async (systemId: any) => {
@@ -80,6 +91,41 @@ export const getAppAccess = async (systemId: any) => {
   });
 };
 
+// 获取消息列表
+export type MessageListParams = {
+  status: number;
+  systemId: string;
+  receiveBeginTime?: string;
+  receiveEndTime?: string;
+  resolveBeginTime?: string;
+  resolveEndTime?: string;
+};
+
+export const getMessageList = async (data: MessageListParams) => {
+  return request('/centerSys/message/getMessageList', {
+    method: 'POST',
+    data,
+    // 查询类接口不需要全局“操作成功”提示
+    skipSuccessMessage: true,
+  });
+};
+
+// 消息批量操作(read-标记已读 / delete-删除等)
+export type MessageBatchParams = {
+  msgIds: Array<string | number>;
+  operation: string;
+  systemId: string | number;
+};
+
+export const batchMessage = async (data: MessageBatchParams) => {
+  return request('/centerSys/message/batch', {
+    method: 'POST',
+    data,
+    // 页面会自行给出更明确的提示文案,避免和全局提示重复
+    skipSuccessMessage: true,
+  });
+};
+
 //删除快速入口
 
 export const removeFastEntrance = async (ids: string[]) => {

+ 13 - 1
src/service/indicator.ts

@@ -103,9 +103,21 @@ export const getIndicatorCateList_old = async (params?: { menuCode: string }) =>
 
 //编辑指标绑定的报告
 
-export const applyOnlineReportMap = async (params?: { code: string,onlineReportCode:string,onlineRemark:string }) => {
+export const applyOnlineReportMap = async (params?: { code: string; onlineReportCode: string; onlineRemark?: string; onlineReportParam?: string }) => {
   return request<any[]>('/centerSys/indicator/applyOnlineReportMap', {
     method: 'POST',
     data: params,
   });
+};
+
+// 统计分析报告绑定
+// 入参说明参考接口文档截图:
+// code: 指标代码
+// analysisReportCode: 统计分析报告代码
+// analysisRemark: 备注
+export const applyStatisticalAnalysisMap = async (params?: { code: string; analysisReportCode: string; analysisRemark?: string; analysisReportParam?: string }) => {
+  return request<any[]>('/centerSys/indicator/applyStatisticalAnalysisMap', {
+    method: 'POST',
+    data: params,
+  });
 };

+ 23 - 1
src/service/login.ts

@@ -1,7 +1,7 @@
 /*
  * @Author: your name
  * @Date: 2021-11-11 10:35:56
- * @LastEditTime: 2023-09-27 15:00:37
+ * @LastEditTime: 2025-06-20 15:50:31
  * @LastEditors: code4eat awesomedema@gmail.com
  * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  * @FilePath: /KC-MiddlePlatform/service/login.ts
@@ -20,6 +20,8 @@ export const getHospConfigBySign = async (hospSign: string) => {
       id: string;
       name: string;
       systemName: string;
+      loginNameDisplayStyle?: string; // 医院名称显示方式:'0'-显示systemName文本,'1'-显示loginNamePicUrl图片
+      loginNamePicUrl?: string; // 医院名称图片URL
     }[]
   >('/centerSys/hospital/getHospConfigBySign', {
     method: 'GET',
@@ -65,6 +67,26 @@ export const login = async (data: LoginApi.LoginParams) => {
   });
 };
 
+// 发送短信验证码
+export const sendSmsCode = async (data: { account: string; hospSign: string }) => {
+  return request<{
+    success: boolean;
+    errorCode?: number;
+    errorMessage?: string;
+  }>('/oauth2/sendSmsCode', {
+    method: 'POST',
+    data: data,
+  });
+};
+
+// 短信登录
+export const smsLogin = async (data: { account: string; hospSign: string; password: string }) => {
+  return request<UserDataType>('/oauth2/smsLogin', {
+    method: 'POST',
+    data: data,
+  });
+};
+
 export const logout = async () => {
   return request('/oauth2/logout', {
     method: 'POST',

+ 1 - 0
src/service/role.ts

@@ -23,6 +23,7 @@ export type RoleItemType = {
   dataPermissionCode?:string;
   dataPermissionName?:string;
   roleTags?:any[]
+  isEdit?: boolean;
 };
 
 type GetAllRolesType = { list: RoleItemType[] } & TableResponseDataType;

+ 8 - 0
src/service/user.ts

@@ -83,6 +83,14 @@ export const editUsersPsd = async (params: { id: number; password: string }) =>
   });
 };
 
+// 校验原密码是否正确(返回 data 为 boolean)
+export const checkPassword = async (params: { userId: number; password: string }) => {
+  return request('/centerSys/user/checkPassword', {
+    method: 'POST',
+    params,
+  });
+};
+
 //获取用户模板
 export const getUsertemplate = async () => {
   return request<string>('/centerSys/user/exportUserTemplate', {