code4eat 3 gadi atpakaļ
revīzija
af26553e78

+ 16 - 0
.editorconfig

@@ -0,0 +1,16 @@
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[Makefile]
+indent_style = tab

+ 20 - 0
.gitignore

@@ -0,0 +1,20 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/npm-debug.log*
+/yarn-error.log
+/yarn.lock
+/package-lock.json
+
+# production
+/dist
+
+# misc
+.DS_Store
+
+# umi
+/src/.umi
+/src/.umi-production
+/src/.umi-test
+/.env.local

+ 8 - 0
.prettierignore

@@ -0,0 +1,8 @@
+**/*.md
+**/*.svg
+**/*.ejs
+**/*.html
+package.json
+.umi
+.umi-production
+.umi-test

+ 11 - 0
.prettierrc

@@ -0,0 +1,11 @@
+{
+  "singleQuote": true,
+  "trailingComma": "all",
+  "printWidth": 80,
+  "overrides": [
+    {
+      "files": ".prettierrc",
+      "options": { "parser": "json" }
+    }
+  ]
+}

+ 19 - 0
.umirc.ts

@@ -0,0 +1,19 @@
+import { defineConfig } from 'umi';
+
+export default defineConfig({
+  nodeModulesTransform: {
+    type: 'none',
+  },
+  routes: [
+    { path: '/', component: '@/pages/index/index' },
+    { path: '/login', component: '@/pages/login/index' },
+  ],
+  proxy:{
+    // '/api': {
+    //   'target': 'http://112.124.59.133:8083/',
+    //   'changeOrigin': true,
+    //   'pathRewrite': { '^/api' : '' },
+    // },
+  },
+  fastRefresh: {},
+});

+ 105 - 0
README.md

@@ -0,0 +1,105 @@
+
+
+![image-20211126105817314](./public/projectlogo.jpg)
+
+
+
+
+
+## 医院中台
+
+##### 背景:
+
+
+
+
+
+##### 安装环境依赖:
+
+```bash
+npm i
+```
+
+or
+
+```bash
+yarn
+```
+
+
+
+##### 启动项目
+
+```bash
+npm run start //包含mock
+npm run start:dev //开发环境,不使用mock,所有数据通过代理接口获取
+
+//更过查看pakage.json
+```
+
+
+
+#### 预览
+
+![image-20211126135739479](./public/preview.jpg)
+
+
+
+
+
+##### 项目结构
+
+```json
+
+.
+├── README.md
+├── package-lock.json
+├── package.json
+├── src
+│   ├── app.tsx
+│   ├── components
+│   │   └── kc-select
+│   ├── global.less
+│   ├── layouts
+│   │   └── index.tsx
+│   ├── pages
+│   │   ├── 404.tsx
+│   │   ├── index
+│   │   └── login
+│   ├── service
+│   │   ├── api.d.ts
+│   │   └── login.ts
+│   └── typings.d.ts
+├── tree.text
+├── tsconfig.json
+├── typings.d.ts
+└── yarn.lock
+
+8 directories, 14 files
+
+```
+
+
+
+#### 创建者有话说
+
+```javascript
+//该项目运用了大量类似关系
+...
+    const [data,setData] = useSate<DataType>();
+
+    useEffect(()=>{
+          //处理相关逻辑
+    },[data]);
+...
+
+//项目数据共享使用的是umi 的model插件,每个page对应一个model.ts文件
+```
+
+
+
+#### RoadMap
+
+- 2022-11-25 
+- 
+

+ 0 - 0
mock/.gitkeep


+ 105 - 0
mock/login.ts

@@ -0,0 +1,105 @@
+/*
+ * @Author: your name
+ * @Date: 2021-11-11 11:46:42
+ * @LastEditTime: 2021-11-19 16:41:56
+ * @LastEditors: Please set LastEditors
+ * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ * @FilePath: /KC-MiddlePlatform/mock/login.ts
+ */
+
+
+
+import { Request, Response } from 'express';
+
+
+const  getList =  (req: Request, res: Response, u: string)=>{
+    const dataSource = [
+         {
+             name:'医院一',
+             value:1
+         },
+         {
+            name:'医院二',
+            value:2
+         },
+         {
+            name:'医院三',
+            value:3
+        }
+    ];
+    const result = {
+      data:{
+          list:dataSource,
+          totalCount:dataSource.length,
+          current:1
+      },
+      success: true,
+      status:200,
+    };
+    
+    return res.json(result); 
+    
+}
+
+const postData =  (req: Request, res: Response, u: string)=>{
+    const {account,password} = req.body;
+    if(account == 'admin'&&password =='123'){
+        const result = {
+            data:{
+                userName:'hasaki'
+            },
+            success: true,
+            status:200,
+        };
+
+        setTimeout(()=>{
+            return res.json(result); 
+        },5000);
+
+    }else {
+        const result = {
+            data:{
+              a:1
+            },
+            success:false,
+            errorMessage:'用户名或密码错误!',
+            status:200,
+        };
+        return res.json(result); 
+    }
+    
+}
+
+const getHospSubSystemList = (req: Request, res: Response,)=>{
+    const result = {
+        data:[
+            {
+                icon:'https://i.postimg.cc/J4fsWx1V/2x.png',
+                id:0,
+                name:'平台管理',
+                url:''
+            },
+            {
+                icon:'https://i.postimg.cc/yNrSZ4pN/2x.png',
+                id:1,
+                name:'追踪方法学',
+                url:''
+            }
+        ],
+        success: true,
+        status:200,
+      };
+      
+      return res.json(result); 
+}
+
+
+
+
+export default {
+    'GET /api/getSubHosp': getList,
+    'POST /api/login': postData,
+    'GET /api/getHospSubSystemList':getHospSubSystemList,
+};
+
+

+ 46 - 0
package.json

@@ -0,0 +1,46 @@
+{
+  "private": true,
+  "scripts": {
+    "start": "umi dev",
+    "build": "umi build",
+    "postinstall": "umi generate tmp",
+    "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
+    "test": "umi-test",
+    "test:coverage": "umi-test --coverage"
+  },
+  "gitHooks": {
+    "pre-commit": "lint-staged"
+  },
+  "lint-staged": {
+    "*.{js,jsx,less,md,json}": [
+      "prettier --write"
+    ],
+    "*.ts?(x)": [
+      "prettier --parser=typescript --write"
+    ]
+  },
+  "dependencies": {
+    "@ant-design/charts": "^1.2.13",
+    "@ant-design/icons": "^4.5.0",
+    "@ant-design/pro-card": "^1.14.17",
+    "@ant-design/pro-descriptions": "^1.6.8",
+    "@ant-design/pro-form": "^1.18.3",
+    "@ant-design/pro-layout": "^6.15.3",
+    "@ant-design/pro-table": "^2.30.8",
+    "react": "17.x",
+    "react-dom": "17.x",
+    "umi": "^3.5.20"
+  },
+  "devDependencies": {
+    "@types/express": "^4.17.13",
+    "@types/react": "^17.0.0",
+    "@types/react-dom": "^17.0.0",
+    "@umijs/preset-react": "1.x",
+    "@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/Mask.png


BIN
public/images/arrow.png


BIN
public/images/arrow_white.png


BIN
public/images/kc-logo.png


BIN
public/images/loginBannner.png


BIN
public/images/notificationIcon.png


BIN
public/images/platformMenu.png


BIN
public/images/tabCloseIcon.png


+ 175 - 0
src/app.tsx

@@ -0,0 +1,175 @@
+/*
+ * @Author: your name
+ * @Date: 2021-11-09 13:57:41
+ * @LastEditTime: 2021-11-29 09:53:18
+ * @LastEditors: Please set LastEditors
+ * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ * @FilePath: /KC-MiddlePlatform/src/app.ts
+ */
+import { PageLoading } from '@ant-design/pro-layout';
+import { notification, } from 'antd';
+import { RequestConfig,history } from 'umi';
+import type { RequestOptionsInit} from 'umi-request';
+
+
+const loginPath = '/login';
+
+let hospSign: string = ''; //医院标识
+
+/** 获取用户信息比较慢的时候会展示一个 loading */
+export const initialStateConfig = {
+        loading: <PageLoading />,
+};
+
+export async function getInitialState(): Promise<{
+        userData?: any,
+        systemLists?:SystemListItem[],  //当前医院可选子系统列表
+        openedSysLists?:SystemListItem[], //当前已打开的系统列表
+}> {
+        const fetchUserInfo = async () => {
+                try {
+                        const userData = localStorage.getItem('KC-MiddlePlatformUserData');
+                        // console.log({ userData });
+                        if (userData) {
+                                return JSON.parse(userData);
+                        }
+                        throw Error;
+                } catch (error) {
+
+                        history.push(`${loginPath}?hospSign=${hospSign}`);
+                }
+                return undefined;
+        };
+
+        const userData = await fetchUserInfo();
+
+        return {
+                userData,
+                openedSysLists:[],
+                systemLists:[
+                     {
+                         id:1,
+                         icon:'https://i.postimg.cc/J4fsWx1V/2x.png',
+                         url:'http://118.31.245.65:8804/user/login/?redirect=%2FDataManagement%2FpublicData&hospSign=tYAoFaa20yCAgaiy',
+                         name:'追踪方法学'
+                     },
+                     {
+                        id:2,
+                        icon:'https://i.postimg.cc/J4fsWx1V/2x.png',
+                        url:'http://118.31.245.65:8804/user/login/?redirect=%2FDataManagement%2FpublicData&hospSign=tYAoFaa20yCAgaiy',
+                        name:'全成本核算'
+                     }
+                ]
+        }
+}
+
+
+
+
+const requestInterceptorsHandle = (url: string, options: RequestOptionsInit) => {
+        return {
+                url: `${url}`,
+                options: { ...options, interceptors: true },
+        };
+}
+
+const responseInterceptorsHandle = async (response: Response, options: RequestOptionsInit) => {
+        const _response: { data: any,status:number,success:boolean } = await response.clone().json();
+        // // console.log({_response});
+        if(_response.success){
+                return _response.data;
+        }else{
+                return _response
+        }
+}
+
+
+interface ErrorInfoStructure {
+        success: boolean; // if request is success
+        data?: any; // response data
+        status?:number,
+        errorCode: string; // code for errorType
+        errorMessage: string; // message display to user 
+        showType?: number; // error display type: 0 silent; 1 message.warn; 2 message.error; 4 notification; 9 page
+        traceId?: string; // Convenient for back-end Troubleshooting: unique request ID
+        host?: string; // Convenient for backend Troubleshooting: host of current access server
+}
+
+
+interface ResponseErr extends Error {
+        data?: any; // 这里是后端返回的原始数据
+        info: ErrorInfoStructure;
+}
+
+
+
+
+const errorHandlerFunc = (error:ResponseErr) => {
+        console.log({error});
+        
+        try {
+                
+                const { info } = error;
+                const errortext = '';
+                const { status, errorMessage } = info
+                notification.error({
+                        message: ` ${status}: ${errorMessage}`,
+                        description: errortext,
+                });
+
+        } catch (err) {
+                console.log({'errorHandlerFunc':err});
+                notification.error({
+                        message:'登录遇到未知错误,查看控制台!',
+                });
+        }
+};
+
+
+export const request: RequestConfig = {
+        timeout: 10000,
+        errorConfig: {
+                adaptor: (resData) => {
+                        console.log({ resData });
+                        if (resData) {
+                                return {
+                                        ...resData,
+                                        success: resData.success,
+                                        errorMessage: resData.errorMessage,
+                                };
+                        } else {
+                                return {
+                                        success: false,
+                                        errorCode: 0,
+                                        status:0,
+                                        errorMessage: '出现未知错误!',
+                                };
+                        }
+                },
+        },
+        errorHandler:(err:any)=>errorHandlerFunc(err),
+        middlewares: [
+                async function middlewareA(ctx, next) {
+                        // console.log('A before');
+                        await next();
+                        // console.log('A after');
+                },
+                async function middlewareB(ctx, next) {
+                        // console.log('B before');
+                        await next();
+                        // console.log('B after');
+                }
+        ],
+        requestInterceptors: [requestInterceptorsHandle],
+        responseInterceptors: [responseInterceptorsHandle],
+};
+
+
+
+
+
+
+
+
+
+

+ 38 - 0
src/components/kc-select/index.tsx

@@ -0,0 +1,38 @@
+/*
+ * @Author: your name
+ * @Date: 2021-11-10 16:32:55
+ * @LastEditTime: 2021-11-11 15:57:39
+ * @LastEditors: Please set LastEditors
+ * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ * @FilePath: /KC-MiddlePlatform/src/components/kc-select/index.tsx
+ */
+import React from 'react'
+
+import Select, { SelectProps, SelectValue } from "antd/lib/select";
+
+
+interface KCSelectProps {
+
+}
+
+class KCSelect<
+  ValueType extends SelectValue = SelectValue
+> extends React.Component<SelectProps<ValueType>> {
+    
+  static defaultProps = {
+    showSearch: false,
+    allowClear: true,
+    optionFilterProp: "children",
+    filterOption: (input: string, option: any) =>
+      option!.children.toLowerCase().indexOf(input.toLowerCase()) >=
+      0,
+  };
+
+  public render() {
+    const { children, ...restProps } = this.props;
+    return <Select {...restProps}>{children}</Select>;
+  }
+}
+
+
+export default KCSelect

+ 27 - 0
src/components/kc-select/typing.d.ts

@@ -0,0 +1,27 @@
+/*
+ * @Author: your name
+ * @Date: 2021-11-11 14:30:19
+ * @LastEditTime: 2021-11-11 14:30:20
+ * @LastEditors: Please set LastEditors
+ * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ * @FilePath: /KC-MiddlePlatform/src/components/kc-select/index.d.ts
+ */
+
+
+declare namespace KCSelectType {
+    type KCSelectPropsType = {
+        data:optionsType[],
+        optionProps?:optionsPropsType,
+        [key:string]:any
+    }
+
+    type optionsPropsType = {
+        //值参考antd Select
+        [key:string]:any
+    }
+
+    type optionsType = {
+         key:string,
+         value:string
+    }
+}

+ 6 - 0
src/global.less

@@ -0,0 +1,6 @@
+
+
+
+input {
+    outline: none !important;
+}

+ 8 - 0
src/layouts/index.tsx

@@ -0,0 +1,8 @@
+/*
+ * @Author: your name
+ * @Date: 2021-11-09 13:56:33
+ * @LastEditTime: 2021-11-09 13:56:33
+ * @LastEditors: Please set LastEditors
+ * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ * @FilePath: /KC-MiddlePlatform/src/layouts/index.tsx
+ */

+ 20 - 0
src/pages/404.tsx

@@ -0,0 +1,20 @@
+/*
+ * @Author: your name
+ * @Date: 2021-11-09 14:23:12
+ * @LastEditTime: 2021-11-09 14:36:13
+ * @LastEditors: your name
+ * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ * @FilePath: /KC-MiddlePlatform/src/pages/404.tsx
+ */
+import { Result } from 'antd';
+import React from 'react';
+
+const NoFoundPage: React.FC = () => (
+  <Result
+    status="404"
+    title="404"
+    subTitle="抱歉,你权限访问该页面或该页面不存在!"
+  />
+);
+
+export default NoFoundPage;

+ 48 - 0
src/pages/index/components/iframePage/index.tsx

@@ -0,0 +1,48 @@
+/*
+ * @Author: your name
+ * @Date: 2021-11-19 15:20:40
+ * @LastEditTime: 2021-11-19 16:13:00
+ * @LastEditors: Please set LastEditors
+ * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ * @FilePath: /KC-MiddlePlatform/src/pages/index/components/iframePage/index.tsx
+ */
+
+import React,{useState,useRef} from "react"
+import { Spin } from 'antd';
+
+import { LoadingOutlined } from '@ant-design/icons';
+
+import './style.less'
+
+export interface IframePage {
+       url:string
+}
+//http://118.31.245.65:8804/user/login/?redirect=%2FDataManagement%2FpublicData&hospSign=tYAoFaa20yCAgaiy
+const IframePage:React.FC<IframePage> = (props)=>{
+    
+    const [loading, setLoading] = useState(true);
+    const {url} = props;
+
+    const onLoadHandle = ()=>{
+        setLoading(false);
+    }
+
+     return (
+        <div className='iframeContainer'>
+        {
+            loading && (
+                  <div className='content'>
+                        <div className='spin'>
+                              <LoadingOutlined style={{ fontSize: 24 }} spin />
+                              <span>努力加载中...</span>
+                        </div>
+                  </div>
+            )
+        }
+        <iframe  onLoad={onLoadHandle} className='iframe' src={url}></iframe>
+        </div>
+     )
+}
+
+
+export default IframePage;

+ 30 - 0
src/pages/index/components/iframePage/style.less

@@ -0,0 +1,30 @@
+
+
+
+.iframeContainer {
+    position: relative;
+    .content {
+        position: absolute;
+        display: flex;
+        top:0;
+        left:0;
+        width: 100%;
+        height: 100%;
+        justify-content: center;
+        align-items: center;
+        height: calc(100vh - 48px);
+        background-color: rgb(238, 237, 237);
+        opacity: 0.8;
+        .spin {
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+        }
+    } 
+    .iframe {
+        width: 100%;
+        height: calc(100vh - 48px);
+        border: none;
+    }
+}

+ 110 - 0
src/pages/index/components/topBar/index.tsx

@@ -0,0 +1,110 @@
+/*
+ * @Author: your name
+ * @Date: 2021-11-16 09:12:37
+ * @LastEditTime: 2021-11-29 14:08:45
+ * @LastEditors: Please set LastEditors
+ * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ * @FilePath: /KC-MiddlePlatform/src/pages/index/components/topBar/index.tsx
+ */
+
+
+import React,{useEffect,useState} from "react";
+import './style.less';
+
+import { Tooltip } from 'antd';
+import {
+    LogoutOutlined,
+    SettingOutlined
+  } from '@ant-design/icons';
+
+import logo from '../../../../../public/images/kc-logo.png';
+import platFormMenuIcon from '../../../../../public/images/platformMenu.png';
+import tabCloseIcon from '../../../../../public/images/tabCloseIcon.png';
+
+interface TopBarType {
+    onTabChange?:(data:SystemListItem) => void, //当tab切换时回调
+    onCurrentSystemChanged?:(data:SystemListItem) => void, //当菜单切换系统时回调
+    sysList?:SystemListItem[],
+    openedTabs:SystemListItem[],
+    currentTab?:SystemListItem,
+}
+
+
+const TopBar:React.FC<TopBarType> = (props)=>{
+    
+    const {onTabChange,onCurrentSystemChanged,sysList=[],openedTabs = [],currentTab} = props;
+    const [systemTabs,setSystemTabs] = useState<SystemListItem[]>([]);  //已打开的tab
+    const [currentSelectedTab,setCurrentSelectedTab] = useState<SystemListItem>();
+    const [ifOpenPannel,setIfOpenPannel] = useState(false);
+    
+    const systemTabClickHandle =(item:SystemListItem)=>{   //导航栏tab点击
+        setCurrentSelectedTab(item);
+        onTabChange&&onTabChange(item);
+    }
+
+    const systemListClickHandle = (data:SystemListItem)=>{   //导航栏系统菜单列表点击回调
+        onCurrentSystemChanged&&onCurrentSystemChanged(data);
+    }
+
+    const userPannelTabClick = (tag:string)=>{
+
+    }
+    
+
+    const UserPannel = ()=>{
+    
+        return (
+            <div className='userPannel'>
+                <div className='userPannelTab' onClick={()=>userPannelTabClick('SETTING')}><SettingOutlined />设置</div>
+                <div className='userPannelTab' onClick={()=>userPannelTabClick('LOGOUT')}><LogoutOutlined />退出</div>
+            </div>
+        )
+    }
+
+    useEffect(()=>{
+        setSystemTabs(openedTabs);
+        currentTab&&setCurrentSelectedTab(currentTab);
+    },[props]);
+    
+    return (
+        <div className='topBar'>
+            <div className='logoWrap'>
+                <img className='logo' src={logo} />
+            </div>
+            <div className={ifOpenPannel?'platformMenu on':'platformMenu'} onClick={()=>setIfOpenPannel(!ifOpenPannel)}>
+                <img src={platFormMenuIcon} />
+                <div className='systemPannel'>
+                       {
+                           sysList.map(item=>(
+                              <div key={item.id} className='systemList' onClick={()=>systemListClickHandle(item)}>
+                                  <img src={item.icon} alt="" />
+                                  <span className='name'>{item.name}</span>
+                              </div>
+                           ))
+                       }
+                </div>
+            </div>
+            <div className='tabWrap'>
+                   {
+                       systemTabs.map(item=>(
+                         <div key={item.id} className={currentSelectedTab?.id == item.id?'tab on':'tab'} onClick={()=>systemTabClickHandle(item)}>{item.name} <img src={tabCloseIcon} alt="close" /></div>
+                       ))
+                   }
+            </div>
+            <div className='userRelaInfoWrap'>
+                   <div className='notification'><img className='notificationIcon' src={require('../../../../../public/images/notificationIcon.png')} /></div>
+                   <Tooltip title={<UserPannel />}  color='#fff' >
+                        <div className='user'>
+                                <img className='avator' src={require('../../../../../public/images/Mask.png')} />
+                                <span className='name'>Mr Lawrence</span>
+                                <img className='arrow' src={require('../../../../../public/images/arrow_white.png')} />
+                        </div>
+                   </Tooltip>
+            </div>
+            
+        </div>
+     )
+}
+
+
+export default TopBar

+ 215 - 0
src/pages/index/components/topBar/style.less

@@ -0,0 +1,215 @@
+.userPannel {
+  width: 100px;
+  background-color: white;
+  .userPannelTab {
+      display: flex;
+      flex-direction: row;
+      justify-content:space-evenly;
+      align-items: center;
+      height:30px;
+      font-size: 14px;
+      font-family: SourceHanSansCN-Normal, SourceHanSansCN;
+      font-weight: 400;
+      color:rgb(131, 127, 127);
+      cursor: pointer;
+      border-bottom: 1px solid #eee;
+
+      &:hover {
+         background-color:#eee;
+      }
+
+      &:last-child {
+        border-bottom: none;
+      }
+  }
+}
+
+
+.topBar {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  width: 100%;
+  height: 48px;
+  padding-left: 16px;
+  background: #3071F2;
+
+  .logoWrap {
+    width: 200px;
+
+    .logo {
+      width: 108px;
+      height: 18px;
+    }
+  }
+
+  .platformMenu {
+    position: relative;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 32px;
+    height: 32px;
+    border-radius: 4px;
+    cursor: pointer;
+    margin-right: 16px;
+
+    img {
+      width: 16px;
+      height: 16px;
+    }
+
+    .systemPannel {
+      display: none;
+      position: absolute;
+      left: 0;
+      top: 50px;
+      z-index: 9;
+      flex-direction: row;
+      justify-content: flex-start;
+      align-items: center;
+      background: #FFFFFF;
+      padding: 16px 20px;
+      box-shadow: 0px 6px 10px 0px rgba(26, 34, 51, 0.2);
+      border-radius: 8px;
+      border: 1px solid #E6EAF2;
+
+      .systemList {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        width: 80px;
+        height: 98px;
+        margin-right: 10px;
+        border-radius: 8px;
+
+        .name {
+          font-size: 12px;
+          font-family: SourceHanSansCN-Normal, SourceHanSansCN;
+          font-weight: 400;
+          color: #666E80;
+        }
+
+        &>img {
+          width: 40px;
+          height: 40px;
+          background: #FFB44D;
+          border-radius: 10px;
+          margin-bottom: 16px;
+        }
+
+        &:last-child {
+          margin-right: 0;
+        }
+
+        &:hover {
+          background: #F7F8FA;
+
+          .name {
+            font-size: 12px;
+            font-family: SourceHanSansCN-Bold, SourceHanSansCN;
+            font-weight: bold;
+            color: #334466;
+          }
+        }
+      }
+    }
+
+    &.on {
+      background: #0B54E6;
+
+      .systemPannel {
+        display: flex;
+      }
+    }
+  }
+
+  .tabWrap {
+    display: flex;
+    flex-direction: row;
+    justify-content: flex-start;
+    align-items: center;
+
+    .tab {
+      padding: 8px 9px;
+      border-radius: 4px;
+      font-size: 14px;
+      font-family: SourceHanSansCN-Medium, SourceHanSansCN;
+      font-weight: 500;
+      color: #FFFFFF;
+      opacity: 0.8;
+      margin-right: 16px;
+      cursor: pointer;
+      box-sizing: border-box;
+
+      &>img {
+        width: 8px;
+        height: 8px;
+        margin-left: 16px;
+      }
+
+      &:hover {
+        background: #5387ee;
+      }
+
+      &.on {
+        opacity: 1;
+        background: #0B54E6;
+      }
+
+      &:last-child {
+        margin-right: 0;
+      }
+    }
+  }
+
+  .userRelaInfoWrap {
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+    align-items: center;
+    position: fixed;
+    right: 0;
+    padding-right: 16px;
+
+    .notification {
+      display: flex;
+      flex-direction: row;
+      justify-content: center;
+      align-items: center;
+      width: 18px;
+      height: 18px;
+      margin-right: 16px;
+      cursor: pointer;
+
+      .notificationIcon {
+        width: 100%;
+      }
+    }
+
+    .user {
+      cursor: pointer;
+
+      .avator {
+        width: 26px;
+        height: 26px;
+        margin-right: 8px;
+      }
+
+      .name {
+        font-size: 14px;
+        font-family: SourceHanSansCN-Normal, SourceHanSansCN;
+        font-weight: 400;
+        color: #FFFFFF;
+        margin-right: 10px;
+      }
+
+      .arrow {
+        width: 11px;
+        height: 7px;
+      }
+    }
+  }
+}

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

@@ -0,0 +1,6 @@
+
+
+
+.indexPage {
+    min-width:1100px;
+}

+ 94 - 0
src/pages/index/index.tsx

@@ -0,0 +1,94 @@
+/*
+ * @Author: your name
+ * @Date: 2021-11-10 09:33:30
+ * @LastEditTime: 2021-11-29 10:13:27
+ * @LastEditors: Please set LastEditors
+ * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ * @FilePath: /KC-MiddlePlatform/src/pages/index.tsx
+ */
+
+
+import React, { useState,useEffect } from 'react';
+import TopBar from './components/topBar';
+import IframePage from './components/iframePage'
+import { useModel } from 'umi';
+import './index.less';
+
+const IndexPage = () => {
+
+      const { 
+            systemLists,  //当前医院可选子系统列表
+            setInitialState,
+            openedSysLists, //当前已打开的系统列表
+            loading,
+            currentSelectedSys, //当前选中的系统
+      } = useModel('@@initialState',model=>{
+          return {
+                systemLists:model.initialState?.systemLists,
+                setInitialState:model.setInitialState,
+                openedSysLists:model.initialState?.openedSysLists,
+                loading:model.loading,
+                currentSelectedSys:model.initialState?.currentSelectedSys,
+            }
+      });
+      const [selectableSysList,setSelectableSysList] = useState<SystemListItem[]>([]);
+      const [openedTabs,setOpenTabs] = useState<SystemListItem[]>();
+      const [currentOpenedTab,setCurrentOpenedTab] = useState<SystemListItem>();
+
+
+      const onTabChangeHandle = ()=>{
+            
+      }
+
+      const onCurrentSystemChanged = async (data:SystemListItem)=>{
+            console.log({'当前选中tab':data});
+            setCurrentOpenedTab(data);
+            const index = openedSysLists?.findIndex(t=>t.id == data.id);
+            if(index != -1){
+                  //现在打开的tab里已经存在
+                  await setInitialState((s)=>{
+                        return {
+                              ...s,
+                              currentSelectedSystem:data
+                        }
+                  });
+            }else {
+                  //新打开的tab
+                  await setInitialState((s)=>{
+                        return {
+                              ...s,
+                              currentSelectedSystem:data,
+                              openedSysLists:openedSysLists?.concat(data)
+                        }
+                  });
+            }
+      }
+
+      useEffect(() => {
+            if(systemLists)setSelectableSysList(systemLists);
+      }, [systemLists]);
+
+
+      useEffect(() => {
+            if(openedSysLists)setOpenTabs(openedSysLists);
+      }, [openedSysLists]);
+
+
+      if(loading)return <h1>Loading...</h1>
+      
+      return (
+            <div className='indexPage'>
+                  <TopBar 
+                        openedTabs={openedTabs?openedTabs:[]}
+                        onTabChange={()=>onTabChangeHandle()} 
+                        onCurrentSystemChanged={data=>onCurrentSystemChanged(data)} 
+                        sysList={selectableSysList} 
+                        currentTab={currentOpenedTab}
+                  />
+                  <IframePage url={`http://118.31.245.65:8804/user/login/?redirect=%2FDataManagement%2FpublicData&hospSign=tYAoFaa20yCAgaiy`} />
+            </div>
+      )
+}
+
+
+export default IndexPage;

+ 191 - 0
src/pages/login/index.tsx

@@ -0,0 +1,191 @@
+/*
+ * @Author: your name
+ * @Date: 2021-11-09 14:58:08
+ * @LastEditTime: 2021-11-19 16:42:22
+ * @LastEditors: Please set LastEditors
+ * @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, message } from 'antd';
+import './style.less'
+
+import { useModel, history } from 'umi';
+
+import { Form, Input, Button, Checkbox } from 'antd';
+
+import logo from '../../../public/images/kc-logo.png'
+
+import KCSelect from '@/components/kc-select';
+
+import { getSubHosp, login,getHospSubSystemList } from '@/service/login';
+
+const { Option } = Select;
+
+const LoginPage: React.FC<any> = () => {
+    const [transitionObj, setTransitionObj] = useState<string>();
+    const [transformObj, setTransformObj] = useState<string>();
+    const loginPageRef = useRef<HTMLDivElement>(null);
+    const [subHospList, setSubHospList] = useState<{ name: string, value: string | number }[]>([]);  //分院列表
+    const [ifSelectSystem,setIfSelectSystem] = useState(false); //是否跳转到选择系统界面
+    const [ifLoading,setIfLoading] = useState(false);
+    const [systemList,setSystemList] = useState<SystemListItem[]>([]);  //可选平台列表
+    const { initialState, setInitialState } = useModel('@@initialState');
+
+
+    const onMouseMoveHandle = (mouseEvent: any) => {
+        //    console.log({mouseEvent});
+        const loginPage = loginPageRef.current;
+        if (loginPage) {
+            let centerX = loginPage.offsetLeft + loginPage.offsetWidth / 2,  //div中心点到页面左边距离
+                centerY = loginPage.offsetTop + loginPage.offsetHeight / 2;
+
+            let deltaX = mouseEvent.pageX - centerX,
+                deltaY = mouseEvent.pageY - centerY;
+
+            let percentageX = deltaX / centerX,  //向左或向右的 偏差率
+                percentageY = deltaY / centerY;
+            let deg = 20;  //控制 偏差的 程度
+            // console.log(percentageX*20,percentageY*20);
+            setTransformObj(`translate(${percentageX * deg}px,${percentageY * deg}px)`);
+            // setTransformObj('rotateX(' + percentageY * deg + 'deg)');
+        }
+
+    }
+
+    const onMouseLeaveHandle = () => {
+        setTransformObj(`translate(0px,0px)`);
+        setTransitionObj('all 0.2s linear');
+    }
+
+    const getSubHospFunc = async () => {
+        const { list } = await getSubHosp();
+        setSubHospList(list);
+    }
+
+    const getHospSubSystemListFunc = async ()=>{
+         const data = await getHospSubSystemList();
+         if(data)setSystemList(data);
+    }
+
+    const onFinish = async (values:LoginPageTypes.LoginInfo) => {
+        setIfLoading(true);
+        const resp = await login(values);
+        setIfLoading(false);
+        if (resp) {
+            message.success('登录成功!');
+            await getHospSubSystemListFunc();
+            await setInitialState((s) => ({ ...s, userData: resp,systemLists:systemList }));
+            localStorage.setItem('KC-MiddlePlatformUserData', JSON.stringify(resp));
+            
+            if(systemList.length>1){
+                //当前医院拥有多个子平台时,进入选择
+                setIfSelectSystem(true);
+            }else {
+                //否则直接进入首页
+                const { query } = history.location;
+                const { redirect } = query as { redirect: string };
+                history.push(redirect || '/');
+                return;
+            }
+            setIfLoading(false);
+            
+        }
+    }
+
+    const selectSysHandle = (systemInfo:LoginPageTypes.SystemListItem)=>{
+        const { query } = history.location;
+        const { redirect } = query as { redirect: string };
+        history.push(redirect || '/');
+    }
+
+    useEffect(() => {
+        //根据hospSign获取分院信息
+        getSubHospFunc();
+    }, []);
+
+
+
+
+    return (
+        <div className='loginPage' onMouseMove={onMouseMoveHandle} onMouseLeave={onMouseLeaveHandle} ref={loginPageRef}>
+            <div className='circleBigOne' style={{ transform: transformObj, transition: transitionObj }}></div>
+            <div className='circleBigerOne' ></div>
+            <div className='circleLittleOne' style={{ transform: transformObj, transition: transitionObj }}></div>
+            <div className='loginBlock'>
+                {
+                    ifSelectSystem ? (
+                        <div className='systemSelector'>
+                            {
+                                systemList.map((t)=>(
+                                      <div className='sysItem' key={t.id} onClick={()=>selectSysHandle(t)}>
+                                          <img className='icon' src={t.icon} alt="" />
+                                          <span className='sysName'>{t.name}</span>
+                                      </div>
+                                ))
+                            }
+                        </div>
+                    ) : (
+                        <>
+                            <div className='left'>
+                                <img className='bannerImg' src={require('../../../public/images/loginBannner.png')} />
+                            </div>
+                            <div className='rightLoginArea'>
+                                <div className='subHospSelector'>
+                                    {
+                                        subHospList.length > 0 && (
+                                            <KCSelect allowClear={false} style={{ width: 200 }} defaultValue={subHospList[0].value} suffixIcon={<img style={{ width: '10px', height: '6px' }} src={require('../../../public/images/arrow.png')} />}>
+                                                {
+                                                    subHospList.map(item => {
+                                                        return (
+                                                            <Option value={item.value} key={item.value}>{item.name}</Option>
+                                                        )
+                                                    })
+                                                }
+                                            </KCSelect>
+                                        )
+                                    }
+                                </div>
+                                <div className='systemName'>欢迎进入医务管理系统</div>
+                                <Form
+                                    onFinish={onFinish}
+                                >
+                                    <Form.Item
+                                        name="account"
+                                        rules={[{ required: true, message: 'Please input your username!' }]}
+                                    >
+                                        <Input className='input' />
+                                    </Form.Item>
+
+                                    <Form.Item
+                                        name="password"
+                                        rules={[{ required: true, message: 'Please input your password!' }]}
+                                    >
+                                        <Input.Password className='input' />
+                                    </Form.Item>
+
+                                    <Form.Item >
+                                        <Button className='loginBtn' type="primary" htmlType="submit" loading={ifLoading} >
+                                            登录
+                                        </Button>
+                                    </Form.Item>
+                                    <Form.Item name="remember" valuePropName="checked">
+                                        <Checkbox className='checkBtn'>记住密码</Checkbox>
+                                    </Form.Item>
+                                </Form>
+                            </div>
+                        </>
+                    )
+                }
+            </div>
+            <div className='bottomLogo'>
+                <img className='logo' src={logo} />
+                <div className='copyright'>© 2021 康程智医(成都)技术部出品</div>
+            </div>
+        </div>
+    )
+}
+
+export default LoginPage;

+ 28 - 0
src/pages/login/login.d.ts

@@ -0,0 +1,28 @@
+/*
+ * @Author: your name
+ * @Date: 2021-11-10 10:05:13
+ * @LastEditTime: 2021-11-19 16:30:08
+ * @LastEditors: Please set LastEditors
+ * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ * @FilePath: /KC-MiddlePlatform/src/pages/login/login.d.ts
+ */
+
+
+
+
+export interface LoginPageTypes {
+
+    //登录后可选平台项
+    type SystemListItem = {
+          icon:string,
+          id:number,
+          name:string
+    }
+
+    //登录发送信息
+    type LoginInfo = {
+        account:string,
+        remember:boolean,
+        password:string
+  }
+}

+ 224 - 0
src/pages/login/style.less

@@ -0,0 +1,224 @@
+.loginPage {
+  position: relative;
+  width: 100%;
+  height: 100vh;
+  min-width: 1100px;
+  background-image: linear-gradient(to right, #3264F2, #3B76F3);
+  overflow: hidden;
+
+  .circleBigOne {
+    width: 300px;
+    height: 300px;
+    margin-top: 12.8%;
+    margin-left: 11%;
+    z-index: 1;
+    border-radius: 50%;
+    background: linear-gradient(126deg, #6699FF 0%, rgba(102, 153, 255, 0) 100%);
+    opacity: 0.6;
+    // transform: rotate(60deg);
+    // background-image: linear-gradient(to right, #5083F9 ,#3C71F5);
+  }
+
+  .circleBigerOne {
+    position: absolute;
+    width: 500px;
+    height: 500px;
+    border-radius: 50%;
+    right: -120px;
+    z-index: 1;
+    transform: rotate(350deg);
+    background: linear-gradient(36deg, #5990FF 0%, rgba(88, 143, 252, 0) 100%);
+    opacity: 0.6;
+  }
+
+  .circleLittleOne {
+    position: absolute;
+    width: 78px;
+    height: 78px;
+    top: 7%;
+    left: 50%;
+    margin-left: -30px;
+    border-radius: 50%;
+    z-index: 1;
+    // transform: rotate(60deg);
+    // background-image: linear-gradient(to right, #5083F9 ,#3C71F5);
+    background: linear-gradient(126deg, #5990FF 0%, rgba(88, 143, 252, 0) 100%);
+    opacity: 0.6;
+
+    &::after {
+      position: absolute;
+      display: block;
+      content: '';
+      width: 80px;
+      height: 22px;
+      top: 88px;
+      background: #0D2980;
+      opacity: 0.1;
+      border-radius: 50%;
+      filter: blur(3px);
+    }
+  }
+
+  .loginBlock {
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+    align-items: center;
+    position: absolute;
+    z-index: 10;
+    width: 800px;
+    height: 400px;
+    left: 50%;
+    top: 50%;
+    margin-left: -400px;
+    margin-top: -200px;
+    background: #FFFFFF;
+    border-radius: 24px;
+    overflow: hidden;
+
+    .systemSelector {
+        display: flex;
+        flex-direction: row;
+        justify-content: center;
+        align-items: center;
+
+        .sysItem {
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+            margin-right: 80px;
+            cursor: pointer;
+            .icon {
+                width: 64px;
+                height: 64px;
+                margin-bottom: 16px;
+            }
+            .sysName {
+                height: 12px;
+                font-size: 12px;
+                font-family: SourceHanSansCN-Normal, SourceHanSansCN;
+                font-weight: 400;
+                color: #334466;
+            }
+
+            &:last-child {
+                margin-right: 0;
+            }
+        }
+    }
+    .left {
+        display: flex;
+        width: 50%;
+        height: 100%;
+        justify-content: center;
+        align-items: center;
+        text-align: center;
+        background: linear-gradient(135deg, #FAFCFF 0%, #F2F9FF 100%);
+        .bannerImg {
+            width: 72%;
+        }
+    }
+    .rightLoginArea {
+        display: flex;
+        width: 50%;
+        height: 100%;
+        flex-direction: column;
+        justify-content:center;
+        align-items: flex-start;
+        padding-left: 60px;
+
+        .subHospSelector {
+            margin-bottom: 10px;
+        }
+        .systemName {
+            font-size: 24px;
+            height: 24px;
+            line-height: 24px;
+            font-family: SourceHanSansCN-Light, SourceHanSansCN;
+            font-weight: 300;
+            color: #1A2233;
+            margin-bottom: 32px;
+        }
+        .input {
+            width: 280px;
+            height: 40px;
+            background: #FFFFFF;
+            border-radius: 4px;
+            border: 1px solid #CFD7E6;
+        }
+
+        .loginBtn {
+            width: 280px;
+            height: 40px;
+            background: #3377FF;
+            border-radius: 4px;
+        }
+
+        .checkBtn {
+            .ant-checkbox-checked .ant-checkbox-inner {
+                background-color: #407FFF;
+                border-color: #407FFF;
+            }
+        }
+
+        .ant-select {
+            border:none;
+            outline: none;
+        }
+        .ant-select-focused {
+            border:none;
+            outline: none;
+        }
+        .ant-select-selector {
+            border:none;
+            outline: none;
+        }
+
+        .ant-select-focused:not(.ant-select-disabled).ant-select:not(.ant-select-customize-input) .ant-select-selector {
+            border-color: transparent;
+            box-shadow: none;
+            padding: 0;
+        }  
+        
+        .ant-select-single:not(.ant-select-customize-input) .ant-select-selector {
+            width: 100%;
+            height: 32px;
+            padding: 0;
+        }
+
+        .ant-select-selection-item {
+            font-size: 14px;
+            font-family: SourceHanSansCN-Normal, SourceHanSansCN;
+            font-weight: 400;
+            color: #666F80;
+        }
+    }
+  }
+
+  .bottomLogo {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    flex-direction: column;
+    position: absolute;
+    z-index: 10;
+    width: 400px;
+    left: 50%;
+    bottom: 40px;
+    margin-left: -200px;
+
+    .logo {
+      width: 144px;
+      height: 24px;
+      margin-bottom:12px;
+    }
+
+    .copyright {
+      font-size: 12px;
+      font-family: SourceHanSansCN-Normal, SourceHanSansCN;
+      font-weight: 400;
+      color: #FFFFFF;
+    }
+  }
+}

+ 40 - 0
src/service/api.d.ts

@@ -0,0 +1,40 @@
+/*
+ * @Author: your name
+ * @Date: 2021-11-11 11:23:56
+ * @LastEditTime: 2021-11-15 17:10:01
+ * @LastEditors: Please set LastEditors
+ * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ * @FilePath: /KC-MiddlePlatform/src/service/api.d.ts
+ */
+
+
+declare namespace LoginApi {
+    type LoginParams = {
+        account:string,
+        password:string,
+        remember:boolean
+      };
+      
+      type CurrentUserData = {
+          token:string,
+          name: string|number;
+          avatar?: string;
+          userid?: string|number;
+          [propsName:string]:any
+      };
+    
+      type LoginResult = {
+        name:string,
+        token:string,
+        [propsName:string]:any
+      }
+    
+      type EditUserInfoType = {
+        account?:string,
+        hospId?:number,
+        hospitalStatus?:number,
+        id?:number,
+        name?:string,
+        password?:string
+      }
+}

+ 33 - 0
src/service/login.ts

@@ -0,0 +1,33 @@
+/*
+ * @Author: your name
+ * @Date: 2021-11-11 10:35:56
+ * @LastEditTime: 2021-11-15 16:56:50
+ * @LastEditors: Please set LastEditors
+ * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ * @FilePath: /KC-MiddlePlatform/service/login.ts
+ */
+
+
+import { request } from 'umi';
+
+export const getSubHosp = async ()=>{
+      return request('/api/getSubHosp',{
+        method: 'GET',
+      })
+}
+
+
+export const getHospSubSystemList = async ()=>{
+    return request('/api/getHospSubSystemList',{
+      method: 'GET',
+    })
+}
+
+export const login  = async (data:LoginApi.LoginParams)=>{
+    
+    return request('/api/login', {
+        method: 'POST',
+        data:data
+    });
+
+}

+ 17 - 0
src/typings.d.ts

@@ -0,0 +1,17 @@
+/*
+ * @Author: your name
+ * @Date: 2021-11-19 16:31:19
+ * @LastEditTime: 2021-11-19 16:34:39
+ * @LastEditors: Please set LastEditors
+ * @Description: 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
+ * @FilePath: /KC-MiddlePlatform/src/typings.d.ts
+ */
+
+
+
+type SystemListItem = {   //平台列表项
+    icon:string,
+    id:number,
+    name:string,
+    url:string,
+}

+ 25 - 0
tree.text

@@ -0,0 +1,25 @@
+.
+├── README.md
+├── package-lock.json
+├── package.json
+├── src
+│   ├── app.tsx
+│   ├── components
+│   │   └── kc-select
+│   ├── global.less
+│   ├── layouts
+│   │   └── index.tsx
+│   ├── pages
+│   │   ├── 404.tsx
+│   │   ├── index
+│   │   └── login
+│   ├── service
+│   │   ├── api.d.ts
+│   │   └── login.ts
+│   └── typings.d.ts
+├── tree.text
+├── tsconfig.json
+├── typings.d.ts
+└── yarn.lock
+
+8 directories, 14 files

+ 37 - 0
tsconfig.json

@@ -0,0 +1,37 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "module": "esnext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "importHelpers": true,
+    "jsx": "react-jsx",
+    "esModuleInterop": true,
+    "sourceMap": true,
+    "baseUrl": "./",
+    "strict": true,
+    "paths": {
+      "@/*": ["src/*"],
+      "@@/*": ["src/.umi/*"]
+    },
+    "allowSyntheticDefaultImports": true
+  },
+  "include": [
+    "mock/**/*",
+    "src/**/*",
+    "config/**/*",
+    ".umirc.ts",
+    "typings.d.ts"
+  ],
+  "exclude": [
+    "node_modules",
+    "lib",
+    "es",
+    "dist",
+    "typings",
+    "**/__test__",
+    "test",
+    "docs",
+    "tests"
+  ]
+}

+ 10 - 0
typings.d.ts

@@ -0,0 +1,10 @@
+declare module '*.css';
+declare module '*.less';
+declare module '*.png';
+declare module '*.svg' {
+  export function ReactComponent(
+    props: React.SVGProps<SVGSVGElement>,
+  ): React.ReactElement;
+  const url: string;
+  export default url;
+}