NeeDaye месяцев назад: 6
Сommit
16fc3224ed
47 измененных файлов с 13717 добавлено и 0 удалено
  1. 3 0
      .eslintrc.js
  2. 13 0
      .gitignore
  3. 8 0
      .idea/.gitignore
  4. 59 0
      .idea/codeStyles/Project.xml
  5. 5 0
      .idea/codeStyles/codeStyleConfig.xml
  6. 12 0
      .idea/edu-rec-expert-manage.iml
  7. 6 0
      .idea/inspectionProfiles/Project_Default.xml
  8. 8 0
      .idea/modules.xml
  9. 6 0
      .idea/prettier.xml
  10. 6 0
      .idea/vcs.xml
  11. 17 0
      .lintstagedrc
  12. 2 0
      .npmrc
  13. 3 0
      .prettierignore
  14. 10 0
      .prettierrc
  15. 3 0
      .stylelintrc.js
  16. 23 0
      .umirc.ts
  17. 3 0
      README.md
  18. 20 0
      mock/userAPI.ts
  19. 29 0
      package.json
  20. 12466 0
      pnpm-lock.yaml
  21. 10 0
      src/access.ts
  22. 47 0
      src/app.ts
  23. 0 0
      src/assets/.gitkeep
  24. 4 0
      src/components/Guide/Guide.less
  25. 23 0
      src/components/Guide/Guide.tsx
  26. 2 0
      src/components/Guide/index.ts
  27. 1 0
      src/constants/index.ts
  28. 6 0
      src/icons/logo.svg
  29. 49 0
      src/layouts/SideMenu.tsx
  30. 36 0
      src/layouts/UserPopContent.tsx
  31. 79 0
      src/layouts/index.tsx
  32. 30 0
      src/layouts/styles/index.less
  33. 50 0
      src/layouts/styles/side-menu.less
  34. 23 0
      src/layouts/styles/user-pop-content.less
  35. 6 0
      src/logo.svg
  36. 22 0
      src/models/global.ts
  37. 26 0
      src/pages/Table/components/CreateForm.tsx
  38. 138 0
      src/pages/Table/components/UpdateForm.tsx
  39. 270 0
      src/pages/Table/index.tsx
  40. 96 0
      src/services/demo/UserController.ts
  41. 7 0
      src/services/demo/index.ts
  42. 68 0
      src/services/demo/typings.d.ts
  43. 5 0
      src/types/LayoutType.ts
  44. 9 0
      src/types/UserType.ts
  45. 4 0
      src/utils/format.ts
  46. 3 0
      tsconfig.json
  47. 1 0
      typings.d.ts

+ 3 - 0
.eslintrc.js

@@ -0,0 +1,3 @@
+module.exports = {
+  extends: require.resolve('@umijs/max/eslint'),
+};

+ 13 - 0
.gitignore

@@ -0,0 +1,13 @@
+/node_modules
+/.env.local
+/.umirc.local.ts
+/config/config.local.ts
+/src/.umi
+/src/.umi-production
+/src/.umi-test
+/.umi
+/.umi-production
+/.umi-test
+/dist
+/.mfsu
+.swc

+ 8 - 0
.idea/.gitignore

@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 59 - 0
.idea/codeStyles/Project.xml

@@ -0,0 +1,59 @@
+<component name="ProjectCodeStyleConfiguration">
+  <code_scheme name="Project" version="173">
+    <HTMLCodeStyleSettings>
+      <option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
+    </HTMLCodeStyleSettings>
+    <JSCodeStyleSettings version="0">
+      <option name="FORCE_SEMICOLON_STYLE" value="true" />
+      <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
+      <option name="USE_DOUBLE_QUOTES" value="false" />
+      <option name="FORCE_QUOTE_STYlE" value="true" />
+      <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
+      <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
+      <option name="SPACES_WITHIN_IMPORTS" value="true" />
+    </JSCodeStyleSettings>
+    <TypeScriptCodeStyleSettings version="0">
+      <option name="FORCE_SEMICOLON_STYLE" value="true" />
+      <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
+      <option name="USE_DOUBLE_QUOTES" value="false" />
+      <option name="FORCE_QUOTE_STYlE" value="true" />
+      <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
+      <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
+      <option name="SPACES_WITHIN_IMPORTS" value="true" />
+    </TypeScriptCodeStyleSettings>
+    <VueCodeStyleSettings>
+      <option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
+      <option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
+    </VueCodeStyleSettings>
+    <codeStyleSettings language="HTML">
+      <option name="SOFT_MARGINS" value="80" />
+      <indentOptions>
+        <option name="INDENT_SIZE" value="2" />
+        <option name="CONTINUATION_INDENT_SIZE" value="2" />
+        <option name="TAB_SIZE" value="2" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="JavaScript">
+      <option name="SOFT_MARGINS" value="80" />
+      <indentOptions>
+        <option name="INDENT_SIZE" value="2" />
+        <option name="CONTINUATION_INDENT_SIZE" value="2" />
+        <option name="TAB_SIZE" value="2" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="TypeScript">
+      <option name="SOFT_MARGINS" value="80" />
+      <indentOptions>
+        <option name="INDENT_SIZE" value="2" />
+        <option name="CONTINUATION_INDENT_SIZE" value="2" />
+        <option name="TAB_SIZE" value="2" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="Vue">
+      <option name="SOFT_MARGINS" value="80" />
+      <indentOptions>
+        <option name="CONTINUATION_INDENT_SIZE" value="2" />
+      </indentOptions>
+    </codeStyleSettings>
+  </code_scheme>
+</component>

+ 5 - 0
.idea/codeStyles/codeStyleConfig.xml

@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
+  </state>
+</component>

+ 12 - 0
.idea/edu-rec-expert-manage.iml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/.tmp" />
+      <excludeFolder url="file://$MODULE_DIR$/temp" />
+      <excludeFolder url="file://$MODULE_DIR$/tmp" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 6 - 0
.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
+  </profile>
+</component>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/edu-rec-expert-manage.iml" filepath="$PROJECT_DIR$/.idea/edu-rec-expert-manage.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
.idea/prettier.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="PrettierConfiguration">
+    <option name="myConfigurationMode" value="AUTOMATIC" />
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>

+ 17 - 0
.lintstagedrc

@@ -0,0 +1,17 @@
+{
+  "*.{md,json}": [
+    "prettier --cache --write"
+  ],
+  "*.{js,jsx}": [
+    "max lint --fix --eslint-only",
+    "prettier --cache --write"
+  ],
+  "*.{css,less}": [
+    "max lint --fix --stylelint-only",
+    "prettier --cache --write"
+  ],
+  "*.ts?(x)": [
+    "max lint --fix --eslint-only",
+    "prettier --cache --parser=typescript --write"
+  ]
+}

+ 2 - 0
.npmrc

@@ -0,0 +1,2 @@
+registry=https://registry.npmmirror.com/
+

+ 3 - 0
.prettierignore

@@ -0,0 +1,3 @@
+node_modules
+.umi
+.umi-production

+ 10 - 0
.prettierrc

@@ -0,0 +1,10 @@
+{
+  "tabWidth": 2,
+  "useTabs": false,
+  "printWidth": 80,
+  "singleQuote": true,
+  "trailingComma": "all",
+  "proseWrap": "never",
+  "overrides": [{ "files": ".prettierrc", "options": { "parser": "json" } }],
+  "plugins": ["prettier-plugin-organize-imports", "prettier-plugin-packagejson"]
+}

+ 3 - 0
.stylelintrc.js

@@ -0,0 +1,3 @@
+module.exports = {
+  extends: require.resolve('@umijs/max/stylelint'),
+};

+ 23 - 0
.umirc.ts

@@ -0,0 +1,23 @@
+import { defineConfig } from '@umijs/max';
+
+export default defineConfig({
+  antd: {},
+  access: {},
+  model: {},
+  initialState: {},
+  request: {},
+  layout: false,
+  routes: [
+    {
+      path: '/',
+      redirect: '/table',
+    },
+    {
+      name: ' CRUD 示例',
+      path: '/table',
+      component: './Table',
+    },
+  ],
+  npmClient: 'pnpm',
+});
+

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# README
+
+`@umijs/max` 模板项目,更多功能参考 [Umi Max 简介](https://umijs.org/docs/max/introduce)

+ 20 - 0
mock/userAPI.ts

@@ -0,0 +1,20 @@
+const users = [
+  { id: 0, name: 'Umi', nickName: 'U', gender: 'MALE' },
+  { id: 1, name: 'Fish', nickName: 'B', gender: 'FEMALE' },
+];
+
+export default {
+  'GET /api/v1/queryUserList': (req: any, res: any) => {
+    res.json({
+      success: true,
+      data: { list: users },
+      errorCode: 0,
+    });
+  },
+  'PUT /api/v1/user/': (req: any, res: any) => {
+    res.json({
+      success: true,
+      errorCode: 0,
+    });
+  },
+};

+ 29 - 0
package.json

@@ -0,0 +1,29 @@
+{
+  "private": true,
+  "author": "NeeDaye <953615108@qq.com>",
+  "scripts": {
+    "dev": "max dev",
+    "build": "max build",
+    "format": "prettier --cache --write .",
+    "prepare": "husky",
+    "postinstall": "max setup",
+    "setup": "max setup",
+    "start": "npm run dev"
+  },
+  "dependencies": {
+    "@ant-design/icons": "^5.0.1",
+    "@ant-design/pro-components": "^2.4.4",
+    "@umijs/max": "^4.4.11",
+    "antd": "^5.4.0"
+  },
+  "devDependencies": {
+    "@types/react": "^18.0.33",
+    "@types/react-dom": "^18.0.11",
+    "husky": "^9",
+    "lint-staged": "^13.2.0",
+    "prettier": "^2.8.7",
+    "prettier-plugin-organize-imports": "^3.2.2",
+    "prettier-plugin-packagejson": "^2.4.3",
+    "typescript": "^5.0.3"
+  }
+}

Разница между файлами не показана из-за своего большого размера
+ 12466 - 0
pnpm-lock.yaml


+ 10 - 0
src/access.ts

@@ -0,0 +1,10 @@
+export default (initialState: API.UserInfo) => {
+  // 在这里按照初始化数据定义项目中的权限,统一管理
+  // 参考文档 https://umijs.org/docs/max/access
+  const canSeeAdmin = !!(
+    initialState && initialState.name !== 'dontHaveAccess'
+  );
+  return {
+    canSeeAdmin,
+  };
+};

+ 47 - 0
src/app.ts

@@ -0,0 +1,47 @@
+import { history } from "@umijs/max";
+
+// 运行时配置
+
+// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
+// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
+export async function getInitialState(): Promise<{ name: string }> {
+  return { name: '@umijs/max' };
+}
+
+/**
+ * 配置请求
+ */
+export const request = {
+  requestInterceptors: [
+    (url: string, options: any) => {
+      options.headers = {
+        ...options.headers,
+        Authorization: `${localStorage.getItem('token') ? 'Bearer ' + localStorage.getItem('token') : ''}`,
+      };
+      return { url, options };
+    },
+  ],
+  errorConfig: {
+    errorHandler: (error: any) => {
+      if (error.response.status === 401) {
+        localStorage.removeItem("token");
+        history.push('/login');
+      }
+    },
+  },
+};
+
+// 配置路由跳转
+export function onRouteChange({ location }: any) {
+  const token = localStorage.getItem('token');
+
+  // 未登录且不在登录页,跳转到登录页
+  if (!token && location.pathname !== '/login') {
+    history.push('/login?redirect=' + encodeURIComponent(location.pathname));
+  }
+
+  // 已登录却访问登录页,跳转到首页
+  if (token && location.pathname === '/login') {
+    history.push('/');
+  }
+}

+ 0 - 0
src/assets/.gitkeep


+ 4 - 0
src/components/Guide/Guide.less

@@ -0,0 +1,4 @@
+.title {
+  margin: 0 auto;
+  font-weight: 200;
+}

+ 23 - 0
src/components/Guide/Guide.tsx

@@ -0,0 +1,23 @@
+import { Layout, Row, Typography } from 'antd';
+import React from 'react';
+import styles from './Guide.less';
+
+interface Props {
+  name: string;
+}
+
+// 脚手架示例组件
+const Guide: React.FC<Props> = (props) => {
+  const { name } = props;
+  return (
+    <Layout>
+      <Row>
+        <Typography.Title level={3} className={styles.title}>
+          欢迎使用 <strong>{name}</strong> !
+        </Typography.Title>
+      </Row>
+    </Layout>
+  );
+};
+
+export default Guide;

+ 2 - 0
src/components/Guide/index.ts

@@ -0,0 +1,2 @@
+import Guide from './Guide';
+export default Guide;

+ 1 - 0
src/constants/index.ts

@@ -0,0 +1 @@
+export const DEFAULT_NAME = 'Umi Max';

+ 6 - 0
src/icons/logo.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" width="39" height="39" style="" filter="none">
+    
+    <g>
+    <path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z" ></path>
+    </g>
+  </svg>

+ 49 - 0
src/layouts/SideMenu.tsx

@@ -0,0 +1,49 @@
+import React, {useState} from 'react';
+import type { MenuProps } from 'antd';
+import { Menu } from 'antd';
+import { ReactComponent as SvgLogo } from '@/icons/logo.svg';
+import { history } from '@umijs/max';
+
+
+import './styles/side-menu.less';
+import {useLocation} from "@@/exports";
+
+type MenuItem = Required<MenuProps>['items'][number];
+
+const items: MenuItem[] = [
+];
+
+interface SideMenuProps {
+    onCollaped: (isExpanded: boolean)=> void,
+    initWidth: number
+}
+
+function SideMenu  ({
+    initWidth = 200,
+}: SideMenuProps) {
+  const location = useLocation();
+  const [selectedKeys, setSelectedKeys] = useState([location.pathname]);
+
+  return (
+    <div style={{ width: initWidth, height: '100vh' }}>
+      <div className='side-header'>
+        <SvgLogo style={{fill: 'currentColor'}} className='header-logo'/>
+        <h3 className='header-title-text'>评审管理系统</h3>
+      </div>
+      <div className='menu-area'>
+        <Menu
+          defaultSelectedKeys={selectedKeys}
+          mode="inline"
+          theme="light"
+          items={items}
+          onClick={(e) => {
+            setSelectedKeys([e.key]);
+            history.push(e.key);
+          }}
+        />
+      </div>
+    </div>
+  );
+}
+
+export default SideMenu;

+ 36 - 0
src/layouts/UserPopContent.tsx

@@ -0,0 +1,36 @@
+import React from 'react';
+import { Button } from 'antd';
+import { LogoutOutlined } from '@ant-design/icons';
+import './styles/user-pop-content.less'; // 引入样式文件
+import { history } from '@umijs/max';
+
+
+
+const UserPopover: React.FC = () => {
+
+  const handleLogout = () => {
+    // 1. 清除token
+    localStorage.removeItem('token');
+
+    // 2. 重置全局状态
+    // setInitialState({ ...initialState, currentUser: null });
+
+    // 3. 跳转到登录页
+    history.push('/login');
+  }
+
+  return (
+    <div className="user-popover-content">
+      <Button
+        block
+        danger
+        icon={<LogoutOutlined />}
+        onClick={handleLogout}
+      >
+        退出登录
+      </Button>
+    </div>
+  );
+};
+
+export default UserPopover;

+ 79 - 0
src/layouts/index.tsx

@@ -0,0 +1,79 @@
+import { CollaspedType } from '@/types/LayoutType';
+import { Outlet, useModel } from '@umijs/max';
+import {Avatar, Layout, Popover, Tabs} from 'antd';
+import { Header } from 'antd/es/layout/layout';
+import React, {useEffect, useState} from 'react';
+import SideMenu from './SideMenu';
+import UserPopover from './UserPopContent';
+import './styles/index.less';
+import {history, useLocation, useRouteProps} from "@@/exports";
+const { Sider, Content } = Layout;
+
+const contentStyle: React.CSSProperties = {
+  textAlign: 'center',
+  minHeight: 120,
+  lineHeight: '120px',
+  color: '#fff',
+};
+
+const siderStyle: React.CSSProperties = {
+  textAlign: 'center',
+  color: '#fff',
+};
+
+const layoutStyle = {
+  overflow: 'hidden',
+  height: '100vh',
+};
+
+
+
+const App: React.FC = () => {
+  const [menuWidth] = useState(CollaspedType.OPEN);
+  const { userInfo } = useModel('global');
+  const routeProps = useRouteProps();
+  const location = useLocation();
+  const [selectedKeys, setSelectedKeys] = useState(location.pathname);
+
+  useEffect(() => {
+    if(location.pathname) {
+      setSelectedKeys(location.pathname);
+    }
+  }, [location.pathname]);
+  return (
+    <Layout style={layoutStyle}>
+      <Sider theme="light" width={menuWidth} style={siderStyle}>
+        <SideMenu
+          initWidth={menuWidth}
+          onCollaped={() => {}}
+        />
+      </Sider>
+      <Layout>
+        <Header className="app-header">
+          <Popover placement="bottom" content={<UserPopover />}>
+            <div className="header-user">
+              <Avatar style={{ backgroundColor: '#fde3cf', color: '#f56a00' }}>
+                {userInfo.realName.charAt(0)}
+              </Avatar>
+              <span className="header-user-name"> {userInfo.realName}</span>
+            </div>
+          </Popover>
+        </Header>
+        <Content style={contentStyle}>
+          <Tabs
+            tabBarStyle={{ backgroundColor: '#fff', padding: '5px 20px 0' }}
+            defaultActiveKey={selectedKeys}
+            items={routeProps.custom}
+            onChange={(e) => {
+              history.push(e);
+            }}
+          />
+          <Outlet />
+        </Content>
+      </Layout>
+    </Layout>
+  );
+};
+
+
+export default App;

+ 30 - 0
src/layouts/styles/index.less

@@ -0,0 +1,30 @@
+.app-header {
+    background-color: @primary-color;
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    .header-user {
+        display: flex;
+        align-items: center;
+        gap: 4px;
+        cursor: pointer;
+        .header-user-name {
+            color: white;
+            font-size: 14px;
+        }
+    }
+}
+
+.ant-pro-page-container-children-container-no-header {
+  padding: unset !important;
+}
+.page-box {
+  height: calc(100vh - 64px);
+  display: flex;
+  flex-direction: column;
+}
+.table-box {
+  flex: 1;
+  overflow: hidden;
+  padding: 10px 20px 20px;
+}

+ 50 - 0
src/layouts/styles/side-menu.less

@@ -0,0 +1,50 @@
+.side-header {
+    padding: 0 4px;
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 64px;
+    border-bottom: 1px solid #CCCCCC;
+    .header-logo {
+        width: 40px;
+        height: 40px;
+        color: @primary-color;
+    }
+    .header-colls {
+        position: absolute;
+        right: -24px;
+        background-color: transparent;
+        color: white;
+        padding: 0;
+        z-index: 40;
+    }
+}
+
+.header-title-text {
+    color: #000000;
+    margin-bottom: 0;
+    flex: 1;
+    text-align: left;
+}
+
+.menu-icon {
+    width: 20px !important;
+    height: 20px !important;
+}
+.menu-area {
+    height: calc(100vh - 65px);
+    overflow-y: auto;
+}
+.ant-menu-item-selected::before {
+    content: ' ';
+    width: 10px;
+    position: absolute;
+    left: 0;
+    top: 0;
+    height: 100%;
+    background-color: @primary-color;
+}
+.ant-menu-title-content {
+    text-align: left;
+}

+ 23 - 0
src/layouts/styles/user-pop-content.less

@@ -0,0 +1,23 @@
+// src/components/UserPopover/index.less
+.user-popover {
+  &-trigger {
+    display: flex;
+    align-items: center;
+    padding: 0 16px;
+    cursor: pointer;
+    &:hover {
+      background-color: rgba(0, 0, 0, 0.025);
+    }
+  }
+
+  &-content {
+    width: 200px;
+    .ant-btn {
+      margin-top: 8px;
+    }
+  }
+
+  &-avatar {
+    margin-right: 8px;
+  }
+}

+ 6 - 0
src/logo.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" width="39" height="39" style="" filter="none">
+    
+    <g>
+    <path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z" ></path>
+    </g>
+  </svg>

+ 22 - 0
src/models/global.ts

@@ -0,0 +1,22 @@
+// 全局共享数据示例
+import { DEFAULT_NAME } from '@/constants';
+import { UserInfoType } from '@/types/UserType';
+import { useState } from 'react';
+
+const useUser = () => {
+  const [name, setName] = useState<string>(DEFAULT_NAME);
+  const userDataTemp = <UserInfoType>{
+    username: 'mx',
+    realName: '谢先生',
+    phoneNumber: '17347315117'
+  };
+  const [userInfo, setUserInfo] = useState<UserInfoType>(userDataTemp);
+  return {
+    name,
+    setName,
+    userInfo,
+    setUserInfo
+  };
+};
+
+export default useUser;

+ 26 - 0
src/pages/Table/components/CreateForm.tsx

@@ -0,0 +1,26 @@
+import { Modal } from 'antd';
+import React, { PropsWithChildren } from 'react';
+
+interface CreateFormProps {
+  modalVisible: boolean;
+  onCancel: () => void;
+}
+
+const CreateForm: React.FC<PropsWithChildren<CreateFormProps>> = (props) => {
+  const { modalVisible, onCancel } = props;
+
+  return (
+    <Modal
+      destroyOnClose
+      title="新建"
+      width={420}
+      open={modalVisible}
+      onCancel={() => onCancel()}
+      footer={null}
+    >
+      {props.children}
+    </Modal>
+  );
+};
+
+export default CreateForm;

+ 138 - 0
src/pages/Table/components/UpdateForm.tsx

@@ -0,0 +1,138 @@
+import {
+  ProFormDateTimePicker,
+  ProFormRadio,
+  ProFormSelect,
+  ProFormText,
+  ProFormTextArea,
+  StepsForm,
+} from '@ant-design/pro-components';
+import { Modal } from 'antd';
+import React from 'react';
+
+export interface FormValueType extends Partial<API.UserInfo> {
+  target?: string;
+  template?: string;
+  type?: string;
+  time?: string;
+  frequency?: string;
+}
+
+export interface UpdateFormProps {
+  onCancel: (flag?: boolean, formVals?: FormValueType) => void;
+  onSubmit: (values: FormValueType) => Promise<void>;
+  updateModalVisible: boolean;
+  values: Partial<API.UserInfo>;
+}
+
+const UpdateForm: React.FC<UpdateFormProps> = (props) => (
+  <StepsForm
+    stepsProps={{
+      size: 'small',
+    }}
+    stepsFormRender={(dom, submitter) => {
+      return (
+        <Modal
+          width={640}
+          bodyStyle={{ padding: '32px 40px 48px' }}
+          destroyOnClose
+          title="规则配置"
+          open={props.updateModalVisible}
+          footer={submitter}
+          onCancel={() => props.onCancel()}
+        >
+          {dom}
+        </Modal>
+      );
+    }}
+    onFinish={props.onSubmit}
+  >
+    <StepsForm.StepForm
+      initialValues={{
+        name: props.values.name,
+        nickName: props.values.nickName,
+      }}
+      title="基本信息"
+    >
+      <ProFormText
+        width="md"
+        name="name"
+        label="规则名称"
+        rules={[{ required: true, message: '请输入规则名称!' }]}
+      />
+      <ProFormTextArea
+        name="desc"
+        width="md"
+        label="规则描述"
+        placeholder="请输入至少五个字符"
+        rules={[
+          { required: true, message: '请输入至少五个字符的规则描述!', min: 5 },
+        ]}
+      />
+    </StepsForm.StepForm>
+    <StepsForm.StepForm
+      initialValues={{
+        target: '0',
+        template: '0',
+      }}
+      title="配置规则属性"
+    >
+      <ProFormSelect
+        width="md"
+        name="target"
+        label="监控对象"
+        valueEnum={{
+          0: '表一',
+          1: '表二',
+        }}
+      />
+      <ProFormSelect
+        width="md"
+        name="template"
+        label="规则模板"
+        valueEnum={{
+          0: '规则模板一',
+          1: '规则模板二',
+        }}
+      />
+      <ProFormRadio.Group
+        name="type"
+        width="md"
+        label="规则类型"
+        options={[
+          {
+            value: '0',
+            label: '强',
+          },
+          {
+            value: '1',
+            label: '弱',
+          },
+        ]}
+      />
+    </StepsForm.StepForm>
+    <StepsForm.StepForm
+      initialValues={{
+        type: '1',
+        frequency: 'month',
+      }}
+      title="设定调度周期"
+    >
+      <ProFormDateTimePicker
+        name="time"
+        label="开始时间"
+        rules={[{ required: true, message: '请选择开始时间!' }]}
+      />
+      <ProFormSelect
+        name="frequency"
+        label="监控对象"
+        width="xs"
+        valueEnum={{
+          month: '月',
+          week: '周',
+        }}
+      />
+    </StepsForm.StepForm>
+  </StepsForm>
+);
+
+export default UpdateForm;

+ 270 - 0
src/pages/Table/index.tsx

@@ -0,0 +1,270 @@
+import services from '@/services/demo';
+import {
+  ActionType,
+  FooterToolbar,
+  PageContainer,
+  ProDescriptions,
+  ProDescriptionsItemProps,
+  ProTable,
+} from '@ant-design/pro-components';
+import { Button, Divider, Drawer, message } from 'antd';
+import React, { useRef, useState } from 'react';
+import CreateForm from './components/CreateForm';
+import UpdateForm, { FormValueType } from './components/UpdateForm';
+
+const { addUser, queryUserList, deleteUser, modifyUser } =
+  services.UserController;
+
+/**
+ * 添加节点
+ * @param fields
+ */
+const handleAdd = async (fields: API.UserInfo) => {
+  const hide = message.loading('正在添加');
+  try {
+    await addUser({ ...fields });
+    hide();
+    message.success('添加成功');
+    return true;
+  } catch (error) {
+    hide();
+    message.error('添加失败请重试!');
+    return false;
+  }
+};
+
+/**
+ * 更新节点
+ * @param fields
+ */
+const handleUpdate = async (fields: FormValueType) => {
+  const hide = message.loading('正在配置');
+  try {
+    await modifyUser(
+      {
+        userId: fields.id || '',
+      },
+      {
+        name: fields.name || '',
+        nickName: fields.nickName || '',
+        email: fields.email || '',
+      },
+    );
+    hide();
+
+    message.success('配置成功');
+    return true;
+  } catch (error) {
+    hide();
+    message.error('配置失败请重试!');
+    return false;
+  }
+};
+
+/**
+ *  删除节点
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.UserInfo[]) => {
+  const hide = message.loading('正在删除');
+  if (!selectedRows) return true;
+  try {
+    await deleteUser({
+      userId: selectedRows.find((row) => row.id)?.id || '',
+    });
+    hide();
+    message.success('删除成功,即将刷新');
+    return true;
+  } catch (error) {
+    hide();
+    message.error('删除失败,请重试');
+    return false;
+  }
+};
+
+const TableList: React.FC<unknown> = () => {
+  const [createModalVisible, handleModalVisible] = useState<boolean>(false);
+  const [updateModalVisible, handleUpdateModalVisible] =
+    useState<boolean>(false);
+  const [stepFormValues, setStepFormValues] = useState({});
+  const actionRef = useRef<ActionType>();
+  const [row, setRow] = useState<API.UserInfo>();
+  const [selectedRowsState, setSelectedRows] = useState<API.UserInfo[]>([]);
+  const columns: ProDescriptionsItemProps<API.UserInfo>[] = [
+    {
+      title: '名称',
+      dataIndex: 'name',
+      tip: '名称是唯一的 key',
+      formItemProps: {
+        rules: [
+          {
+            required: true,
+            message: '名称为必填项',
+          },
+        ],
+      },
+    },
+    {
+      title: '昵称',
+      dataIndex: 'nickName',
+      valueType: 'text',
+    },
+    {
+      title: '性别',
+      dataIndex: 'gender',
+      hideInForm: true,
+      valueEnum: {
+        0: { text: '男', status: 'MALE' },
+        1: { text: '女', status: 'FEMALE' },
+      },
+    },
+    {
+      title: '操作',
+      dataIndex: 'option',
+      valueType: 'option',
+      render: (_, record) => (
+        <>
+          <a
+            onClick={() => {
+              handleUpdateModalVisible(true);
+              setStepFormValues(record);
+            }}
+          >
+            配置
+          </a>
+          <Divider type="vertical" />
+          <a href="">订阅警报</a>
+        </>
+      ),
+    },
+  ];
+
+  return (
+    <PageContainer
+      header={{
+        title: 'CRUD 示例',
+      }}
+    >
+      <ProTable<API.UserInfo>
+        headerTitle="查询表格"
+        actionRef={actionRef}
+        rowKey="id"
+        search={{
+          labelWidth: 120,
+        }}
+        toolBarRender={() => [
+          <Button
+            key="1"
+            type="primary"
+            onClick={() => handleModalVisible(true)}
+          >
+            新建
+          </Button>,
+        ]}
+        request={async (params, sorter, filter) => {
+          const { data, success } = await queryUserList({
+            ...params,
+            // FIXME: remove @ts-ignore
+            // @ts-ignore
+            sorter,
+            filter,
+          });
+          return {
+            data: data?.list || [],
+            success,
+          };
+        }}
+        columns={columns}
+        rowSelection={{
+          onChange: (_, selectedRows) => setSelectedRows(selectedRows),
+        }}
+      />
+      {selectedRowsState?.length > 0 && (
+        <FooterToolbar
+          extra={
+            <div>
+              已选择{' '}
+              <a style={{ fontWeight: 600 }}>{selectedRowsState.length}</a>{' '}
+              项&nbsp;&nbsp;
+            </div>
+          }
+        >
+          <Button
+            onClick={async () => {
+              await handleRemove(selectedRowsState);
+              setSelectedRows([]);
+              actionRef.current?.reloadAndRest?.();
+            }}
+          >
+            批量删除
+          </Button>
+          <Button type="primary">批量审批</Button>
+        </FooterToolbar>
+      )}
+      <CreateForm
+        onCancel={() => handleModalVisible(false)}
+        modalVisible={createModalVisible}
+      >
+        <ProTable<API.UserInfo, API.UserInfo>
+          onSubmit={async (value) => {
+            const success = await handleAdd(value);
+            if (success) {
+              handleModalVisible(false);
+              if (actionRef.current) {
+                actionRef.current.reload();
+              }
+            }
+          }}
+          rowKey="id"
+          type="form"
+          columns={columns}
+        />
+      </CreateForm>
+      {stepFormValues && Object.keys(stepFormValues).length ? (
+        <UpdateForm
+          onSubmit={async (value) => {
+            const success = await handleUpdate(value);
+            if (success) {
+              handleUpdateModalVisible(false);
+              setStepFormValues({});
+              if (actionRef.current) {
+                actionRef.current.reload();
+              }
+            }
+          }}
+          onCancel={() => {
+            handleUpdateModalVisible(false);
+            setStepFormValues({});
+          }}
+          updateModalVisible={updateModalVisible}
+          values={stepFormValues}
+        />
+      ) : null}
+
+      <Drawer
+        width={600}
+        open={!!row}
+        onClose={() => {
+          setRow(undefined);
+        }}
+        closable={false}
+      >
+        {row?.name && (
+          <ProDescriptions<API.UserInfo>
+            column={2}
+            title={row?.name}
+            request={async () => ({
+              data: row || {},
+            })}
+            params={{
+              id: row?.name,
+            }}
+            columns={columns}
+          />
+        )}
+      </Drawer>
+    </PageContainer>
+  );
+};
+
+export default TableList;

+ 96 - 0
src/services/demo/UserController.ts

@@ -0,0 +1,96 @@
+/* eslint-disable */
+// 该文件由 OneAPI 自动生成,请勿手动修改!
+import { request } from '@umijs/max';
+
+/** 此处后端没有提供注释 GET /api/v1/queryUserList */
+export async function queryUserList(
+  params: {
+    // query
+    /** keyword */
+    keyword?: string;
+    /** current */
+    current?: number;
+    /** pageSize */
+    pageSize?: number;
+  },
+  options?: { [key: string]: any },
+) {
+  return request<API.Result_PageInfo_UserInfo__>('/api/v1/queryUserList', {
+    method: 'GET',
+    params: {
+      ...params,
+    },
+    ...(options || {}),
+  });
+}
+
+/** 此处后端没有提供注释 POST /api/v1/user */
+export async function addUser(
+  body?: API.UserInfoVO,
+  options?: { [key: string]: any },
+) {
+  return request<API.Result_UserInfo_>('/api/v1/user', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** 此处后端没有提供注释 GET /api/v1/user/${param0} */
+export async function getUserDetail(
+  params: {
+    // path
+    /** userId */
+    userId?: string;
+  },
+  options?: { [key: string]: any },
+) {
+  const { userId: param0 } = params;
+  return request<API.Result_UserInfo_>(`/api/v1/user/${param0}`, {
+    method: 'GET',
+    params: { ...params },
+    ...(options || {}),
+  });
+}
+
+/** 此处后端没有提供注释 PUT /api/v1/user/${param0} */
+export async function modifyUser(
+  params: {
+    // path
+    /** userId */
+    userId?: string;
+  },
+  body?: API.UserInfoVO,
+  options?: { [key: string]: any },
+) {
+  const { userId: param0 } = params;
+  return request<API.Result_UserInfo_>(`/api/v1/user/${param0}`, {
+    method: 'PUT',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    params: { ...params },
+    data: body,
+    ...(options || {}),
+  });
+}
+
+/** 此处后端没有提供注释 DELETE /api/v1/user/${param0} */
+export async function deleteUser(
+  params: {
+    // path
+    /** userId */
+    userId?: string;
+  },
+  options?: { [key: string]: any },
+) {
+  const { userId: param0 } = params;
+  return request<API.Result_string_>(`/api/v1/user/${param0}`, {
+    method: 'DELETE',
+    params: { ...params },
+    ...(options || {}),
+  });
+}

+ 7 - 0
src/services/demo/index.ts

@@ -0,0 +1,7 @@
+/* eslint-disable */
+// 该文件由 OneAPI 自动生成,请勿手动修改!
+
+import * as UserController from './UserController';
+export default {
+  UserController,
+};

+ 68 - 0
src/services/demo/typings.d.ts

@@ -0,0 +1,68 @@
+/* eslint-disable */
+// 该文件由 OneAPI 自动生成,请勿手动修改!
+
+declare namespace API {
+  interface PageInfo {
+    /** 
+1 */
+    current?: number;
+    pageSize?: number;
+    total?: number;
+    list?: Array<Record<string, any>>;
+  }
+
+  interface PageInfo_UserInfo_ {
+    /** 
+1 */
+    current?: number;
+    pageSize?: number;
+    total?: number;
+    list?: Array<UserInfo>;
+  }
+
+  interface Result {
+    success?: boolean;
+    errorMessage?: string;
+    data?: Record<string, any>;
+  }
+
+  interface Result_PageInfo_UserInfo__ {
+    success?: boolean;
+    errorMessage?: string;
+    data?: PageInfo_UserInfo_;
+  }
+
+  interface Result_UserInfo_ {
+    success?: boolean;
+    errorMessage?: string;
+    data?: UserInfo;
+  }
+
+  interface Result_string_ {
+    success?: boolean;
+    errorMessage?: string;
+    data?: string;
+  }
+
+  type UserGenderEnum = 'MALE' | 'FEMALE';
+
+  interface UserInfo {
+    id?: string;
+    name?: string;
+    /** nick */
+    nickName?: string;
+    /** email */
+    email?: string;
+    gender?: UserGenderEnum;
+  }
+
+  interface UserInfoVO {
+    name?: string;
+    /** nick */
+    nickName?: string;
+    /** email */
+    email?: string;
+  }
+
+  type definitions_0 = null;
+}

+ 5 - 0
src/types/LayoutType.ts

@@ -0,0 +1,5 @@
+/** 菜单展开状态枚举 */
+export enum CollaspedType {
+    CLOSE = 68,
+    OPEN = 226
+}

+ 9 - 0
src/types/UserType.ts

@@ -0,0 +1,9 @@
+/**
+ * 定义用户信息类型
+ */
+export type UserInfoType  = {
+    username: string;
+    realName: string;
+    phoneNumber: string;
+    avatar: string;
+}

+ 4 - 0
src/utils/format.ts

@@ -0,0 +1,4 @@
+// 示例方法,没有实际意义
+export function trim(str: string) {
+  return str.trim();
+}

+ 3 - 0
tsconfig.json

@@ -0,0 +1,3 @@
+{
+  "extends": "./src/.umi/tsconfig.json"
+}

+ 1 - 0
typings.d.ts

@@ -0,0 +1 @@
+import '@umijs/max/typings';