Selaa lähdekoodia

初始化融合平台

bill 3 vuotta sitten
vanhempi
commit
d15daad7e3
51 muutettua tiedostoa jossa 10759 lisäystä ja 171 poistoa
  1. 11 0
      package.json
  2. 0 25
      public/index.html
  3. BIN
      public/logo192.png
  4. BIN
      public/logo512.png
  5. 0 25
      public/manifest.json
  6. 0 3
      public/robots.txt
  7. 0 38
      src/App.css
  8. 0 9
      src/App.test.tsx
  9. 0 26
      src/App.tsx
  10. 9 0
      src/api/axios.ts
  11. 5 0
      src/api/index.ts
  12. 18 0
      src/api/scene.ts
  13. 184 0
      src/api/setup.ts
  14. 30 0
      src/components/async-component/index.tsx
  15. 3 0
      src/components/index.ts
  16. 44 0
      src/components/route-menu.tsx
  17. 32 0
      src/components/tabs/index.tsx
  18. 14 0
      src/components/tabs/style.module.scss
  19. 9 0
      src/constant/api.ts
  20. 11 0
      src/constant/index.ts
  21. 33 0
      src/constant/scene.ts
  22. 14 0
      src/constant/thunk.ts
  23. 25 0
      src/hook/index.ts
  24. 0 13
      src/index.css
  25. 7 9
      src/index.tsx
  26. 0 0
      src/layout/example/index.tsx
  27. 15 0
      src/layout/header/index.tsx
  28. 16 0
      src/layout/header/style.module.scss
  29. 40 0
      src/layout/top/index.tsx
  30. 12 0
      src/layout/top/style.module.scss
  31. 0 1
      src/logo.svg
  32. 11 0
      src/public.scss
  33. 22 0
      src/react-app-env.d.ts
  34. 0 15
      src/reportWebVitals.ts
  35. 68 0
      src/router/config.ts
  36. 30 0
      src/router/index.tsx
  37. 0 5
      src/setupTests.ts
  38. 68 0
      src/store/help.ts
  39. 27 0
      src/store/index.tsx
  40. 42 0
      src/store/scene.ts
  41. 50 0
      src/utils/array.ts
  42. 3 0
      src/utils/base.ts
  43. 3 0
      src/utils/index.ts
  44. 20 0
      src/utils/route.ts
  45. 10 0
      src/views/login/index.tsx
  46. 25 0
      src/views/scene/index.tsx
  47. 34 0
      src/views/scene/list.tsx
  48. 3 0
      src/views/scene/style.module.scss
  49. 66 0
      src/views/scene/table-cloumns.tsx
  50. 3 2
      tsconfig.json
  51. 9742 0
      yarn.lock

+ 11 - 0
package.json

@@ -3,16 +3,27 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@ant-design/icons": "^4.7.0",
+    "@reduxjs/toolkit": "^1.8.3",
     "@testing-library/jest-dom": "^5.16.4",
     "@testing-library/react": "^13.3.0",
     "@testing-library/user-event": "^13.5.0",
     "@types/jest": "^27.5.2",
+    "@types/lodash": "^4.14.182",
     "@types/node": "^16.11.45",
     "@types/react": "^18.0.15",
     "@types/react-dom": "^18.0.6",
+    "antd": "^4.21.7",
+    "axios": "^0.27.2",
+    "craco-less": "^2.0.0",
+    "lodash": "^4.17.21",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
+    "react-redux": "^8.0.2",
+    "react-router-dom": "^6.3.0",
     "react-scripts": "5.0.1",
+    "redux": "^4.2.0",
+    "sass": "^1.54.0",
     "typescript": "^4.7.4",
     "web-vitals": "^2.1.4"
   },

+ 0 - 25
public/index.html

@@ -9,35 +9,10 @@
       name="description"
       content="Web site created using create-react-app"
     />
-    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
-    <!--
-      manifest.json provides metadata used when your web app is installed on a
-      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-    -->
-    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
-    <!--
-      Notice the use of %PUBLIC_URL% in the tags above.
-      It will be replaced with the URL of the `public` folder during the build.
-      Only files inside the `public` folder can be referenced from the HTML.
-
-      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
-      work correctly both with client-side routing and a non-root public URL.
-      Learn how to configure a non-root public URL by running `npm run build`.
-    -->
     <title>React App</title>
   </head>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>
     <div id="root"></div>
-    <!--
-      This HTML file is a template.
-      If you open it directly in the browser, you will see an empty page.
-
-      You can add webfonts, meta tags, or analytics to this file.
-      The build step will place the bundled scripts into the <body> tag.
-
-      To begin the development, run `npm start` or `yarn start`.
-      To create a production bundle, use `npm run build` or `yarn build`.
-    -->
   </body>
 </html>

BIN
public/logo192.png


BIN
public/logo512.png


+ 0 - 25
public/manifest.json

@@ -1,25 +0,0 @@
-{
-  "short_name": "React App",
-  "name": "Create React App Sample",
-  "icons": [
-    {
-      "src": "favicon.ico",
-      "sizes": "64x64 32x32 24x24 16x16",
-      "type": "image/x-icon"
-    },
-    {
-      "src": "logo192.png",
-      "type": "image/png",
-      "sizes": "192x192"
-    },
-    {
-      "src": "logo512.png",
-      "type": "image/png",
-      "sizes": "512x512"
-    }
-  ],
-  "start_url": ".",
-  "display": "standalone",
-  "theme_color": "#000000",
-  "background_color": "#ffffff"
-}

+ 0 - 3
public/robots.txt

@@ -1,3 +0,0 @@
-# https://www.robotstxt.org/robotstxt.html
-User-agent: *
-Disallow:

+ 0 - 38
src/App.css

@@ -1,38 +0,0 @@
-.App {
-  text-align: center;
-}
-
-.App-logo {
-  height: 40vmin;
-  pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
-  .App-logo {
-    animation: App-logo-spin infinite 20s linear;
-  }
-}
-
-.App-header {
-  background-color: #282c34;
-  min-height: 100vh;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  font-size: calc(10px + 2vmin);
-  color: white;
-}
-
-.App-link {
-  color: #61dafb;
-}
-
-@keyframes App-logo-spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}

+ 0 - 9
src/App.test.tsx

@@ -1,9 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
-  render(<App />);
-  const linkElement = screen.getByText(/learn react/i);
-  expect(linkElement).toBeInTheDocument();
-});

+ 0 - 26
src/App.tsx

@@ -1,26 +0,0 @@
-import React from 'react';
-import logo from './logo.svg';
-import './App.css';
-
-function App() {
-  return (
-    <div className="App">
-      <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.tsx</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
-      </header>
-    </div>
-  );
-}
-
-export default App;

+ 9 - 0
src/api/axios.ts

@@ -0,0 +1,9 @@
+import { axiosFactory } from './setup'
+
+const { axios } = axiosFactory()
+
+export {
+  axios
+}
+
+export default axios

+ 5 - 0
src/api/index.ts

@@ -0,0 +1,5 @@
+
+
+export { ResCode } from 'constant'
+export * from './scene'
+export * from './axios'

+ 18 - 0
src/api/scene.ts

@@ -0,0 +1,18 @@
+import { SceneStatus } from 'constant'
+import axios from './axios'
+
+
+export interface Scene {
+  id: string,
+  title: string
+  sncode: string,
+  time: string,
+  status: SceneStatus,
+}
+
+export type Scenes = Scene[]
+
+export const getSceneByType = async (keyword?: string) => {
+  const scenes = await axios.get<Scenes>('/scenes', { params: { keyword } })
+  return scenes
+}

+ 184 - 0
src/api/setup.ts

@@ -0,0 +1,184 @@
+import Axios from 'axios'
+import { ResCode } from 'constant'
+
+import type { AxiosResponse, AxiosRequestConfig } from 'axios'
+
+export type ResErrorHandler = <D, T extends ResData<D>>(response: AxiosResponse<T>, data?: T) => void
+export type ReqErrorHandler = <T>(err: Error, response: AxiosRequestConfig<T>) => void
+export type ResData<T> = { code: ResCode, msg: string, data: T }
+
+export const axiosFactory = () => {
+  const axiosRaw = Axios.create()
+  const axiosConfig = {
+    token: localStorage.getItem('token'),
+    unTokenSet: [] as string[],
+    unReqErrorSet: [] as string[],
+    unResErrorSet: [] as string[],
+    resErrorHandler: [] as ResErrorHandler[],
+    reqErrorHandler: [] as ReqErrorHandler[],
+  }
+
+
+  type AxiosConfig = typeof axiosConfig
+  type ExponseApi<K extends keyof AxiosConfig> = 
+    { set: (val: AxiosConfig[K]) => void } 
+      & (AxiosConfig[K] extends Array<any> 
+        ? { 
+            add: (val: AxiosConfig[K]) => void,
+            del: (val?: AxiosConfig[K]) => void
+          }
+        : { del: () => void })
+
+  const getExponseApi = <K extends keyof AxiosConfig>(key: K): ExponseApi<K> => {
+    const axiosObj: any = axiosConfig[key]
+    const apis: any = { 
+      set (val: AxiosConfig[K]) {
+        axiosConfig[key] = val
+      }
+    }
+    
+    if (Array.isArray(axiosObj)) {
+      apis.add = (val: AxiosConfig[K]) => {
+        axiosObj.push(...val as any)
+      }
+      apis.del = (val?: AxiosConfig[K]) => {
+        if (val) {
+          axiosConfig[key] = axiosObj.filter(item => !val?.includes(item)) as any
+        } else {
+          axiosConfig[key] = [] as any  
+        }
+      }
+    } else {
+      apis.del = () => {
+        axiosConfig[key] = undefined as any
+      }
+    }
+    return apis
+  }
+
+  const {
+    set: setToken,
+    del: delToken,
+  } = getExponseApi('token')
+
+  const {
+    set: setUnsetTokenURLS,
+    add: addUnsetTokenURLS,
+    del: delUnsetTokenURLS
+  } = getExponseApi('unTokenSet')
+
+  const {
+    set: setResErrorHandler,
+    add: addResErrorHandler,
+    del: delResErrorHandler
+  } = getExponseApi('resErrorHandler')
+
+  const {
+    set: setUnsetReqErrorURLS,
+    add: addUnsetReqErrorURLS,
+    del: delUnsetReqErrorURLS
+  } = getExponseApi('unReqErrorSet')
+
+  const {
+    set: setReqErrorHandler,
+    add: addReqErrorHandler,
+    del: delReqErrorHandler
+  } = getExponseApi('reqErrorHandler')
+
+  const {
+    set: setUnsetResErrorURLS,
+    add: addUnsetResErrorURLS,
+    del: delUnsetResErrorURLS
+  } = getExponseApi('unResErrorSet')
+
+  const setDefaultURI = (url: string) => {
+    axiosRaw.defaults.baseURL = url
+  }
+
+  const matchURL = (urls: string[], config: AxiosRequestConfig<any>) => 
+    config.url && urls.includes(config.url)
+
+  axiosRaw.interceptors.request.use(config => {
+    if (!matchURL(axiosConfig.unTokenSet, config)) {
+      if (!axiosConfig.token) {
+        if (!matchURL(axiosConfig.unReqErrorSet, config)) {
+          const error = new Error('缺少token')
+          axiosConfig.reqErrorHandler.forEach(handler => handler(error, config))
+          throw error
+        }
+      } else {
+        config.headers = {
+          ...config.headers,
+          token: axiosConfig.token
+        }
+      }
+    }
+    return config
+  })
+
+  axiosRaw.interceptors.response.use((response: AxiosResponse<ResData<any>>) => {
+    if (matchURL(axiosConfig.unResErrorSet, response.config)) {
+      return response
+    }
+
+    if (response.status !== 200) {
+      axiosConfig.resErrorHandler.forEach(handler => handler(response))
+      throw new Error(response.statusText)
+    } else if (response.data.code !== ResCode.SUCCESS) {
+      axiosConfig.resErrorHandler.forEach(handler => handler(response, response.data))
+      if (response.data.code === ResCode.TOKEN_INVALID) {
+        delToken()
+      }
+      throw new Error(response.data.msg)
+    } else {
+      return response.data.data
+    }
+  })
+
+
+  type AxiosProcess = {
+    getUri(config?: AxiosRequestConfig): string;
+    request<R = any, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
+    get<R = any, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
+    delete<R = any, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
+    head<R = any, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
+    options<R = any, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
+    post<R = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
+    put<R = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
+    patch<R = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
+    postForm<R = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
+    putForm<R = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
+    patchForm<R = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
+  }
+
+  interface AxiosInstanceProcess extends AxiosProcess {
+    <R = any>(config: AxiosRequestConfig): Promise<R>;
+    <R = any>(url: string, config?: AxiosRequestConfig): Promise<R>;
+  }
+
+  const axios: AxiosInstanceProcess = axiosRaw as any
+
+  return {
+    axios,
+    setToken,
+    delToken,
+    setUnsetTokenURLS,
+    addUnsetTokenURLS,
+    delUnsetTokenURLS,
+    setResErrorHandler,
+    addResErrorHandler,
+    delResErrorHandler,
+    setUnsetReqErrorURLS,
+    addUnsetReqErrorURLS,
+    delUnsetReqErrorURLS,
+    setReqErrorHandler,
+    addReqErrorHandler,
+    delReqErrorHandler,
+    setUnsetResErrorURLS,
+    addUnsetResErrorURLS,
+    delUnsetResErrorURLS,
+    setDefaultURI
+  }
+}
+
+export default axiosFactory

+ 30 - 0
src/components/async-component/index.tsx

@@ -0,0 +1,30 @@
+import { 
+  LazyExoticComponent, 
+  Suspense, 
+  ComponentType,
+  ReactElement,
+  ComponentPropsWithRef,
+} from 'react'
+
+import { LoadingOutlined } from '@ant-design/icons'
+import { Spin } from 'antd'
+
+export type AsyncComponentProps<T extends ComponentType<any>> = {
+  mount: LazyExoticComponent<T>
+  loading?: ReactElement
+} &  ComponentPropsWithRef<T>
+
+
+export const Loading = () => (
+  <Spin 
+    indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} 
+  />
+)
+
+export const AsyncComponent = <T extends ComponentType<any>>(
+  { mount: Mount, loading, ...props }: AsyncComponentProps<T>
+) => (
+  <Suspense fallback={loading || <Loading />}>
+    <Mount {...props} />
+  </Suspense>
+)

+ 3 - 0
src/components/index.ts

@@ -0,0 +1,3 @@
+export * from './async-component'
+export * from './route-menu'
+export * from './tabs'

+ 44 - 0
src/components/route-menu.tsx

@@ -0,0 +1,44 @@
+import { Menu as MenuRaw } from 'antd'
+import { useRoute, findRouteByPathName } from 'router'
+
+import type { MenuProps } from 'antd'
+import type { RouteConfig, RouteConfigTree } from 'router'
+
+type MenuItems = Required<MenuProps>['items']
+type MenuProp = { 
+  routes: RouteConfig[] | undefined,
+  onSelect?: (route: RouteConfigTree) => void
+}
+
+export const routesToMenuItems = (routes: MenuProp['routes']): MenuItems | undefined => {
+  if (!routes) {
+    return;
+  }
+  return routes
+    .filter(route => route.meta)
+    .map(route => ({
+      ...route.meta,
+      icon: route.meta?.icon && <route.meta.icon />,
+      key: route.path,
+      children: routesToMenuItems(route.children)
+    }))
+}
+
+export const RouteMenu = ({ routes, onSelect }: MenuProp) => {
+  const route = useRoute()
+  const menuItems = routesToMenuItems(routes)
+  const onClick: MenuProps['onClick'] = e => {
+    const routeTree = findRouteByPathName(e.key) as RouteConfigTree
+    onSelect && onSelect(routeTree)
+  };
+  const selectKeys = route?.path ? [route?.path] : []
+
+  return (
+    <MenuRaw
+      onClick={onClick}
+      defaultSelectedKeys={selectKeys}
+      mode="inline"
+      items={menuItems}
+    />
+  )
+}

+ 32 - 0
src/components/tabs/index.tsx

@@ -0,0 +1,32 @@
+import { Tabs as ATabs } from 'antd'
+import style from './style.module.scss'
+
+import type { ReactNode, Key } from 'react'
+
+const { TabPane: ATabPane } = ATabs
+
+export type TabsProps<T> = { 
+  onChange: (type: string) => void, 
+  active: T,
+  content: ReactNode,
+  items: Array<readonly [T, string]>
+}
+
+export const Tabs = <T extends Key>({ onChange, active, content, items }: TabsProps<T>) => {
+  const renderOptions =items.map(([key, value]) => (
+      <ATabPane tab={value} key={key} className={style['tab-panel']}>
+        {content}
+      </ATabPane>
+    ))
+
+  return (
+    <div className={style['tab']}>
+      <ATabs 
+        onChange={key => onChange(key)} 
+        activeKey={active.toString()}
+      >
+        {renderOptions}
+      </ATabs>
+    </div>
+  )
+}

+ 14 - 0
src/components/tabs/style.module.scss

@@ -0,0 +1,14 @@
+.tab {
+  :global(.ant-tabs-nav) {
+    background: #fff;
+    padding: 0 24px;
+  }
+
+  :global(.ant-tabs-content) {
+    padding: 0 25px;
+  }
+
+  .tab-panel {
+    background: #fff;
+  }
+}

+ 9 - 0
src/constant/api.ts

@@ -0,0 +1,9 @@
+export enum ResCode {
+  TOKEN_INVALID = 403,
+  SUCCESS = 0
+}
+
+export const ResCodeDesc: { [key in ResCode]: string } = {
+  [ResCode.TOKEN_INVALID]: 'token已失效',
+  [ResCode.SUCCESS]: '请求成功'
+}

+ 11 - 0
src/constant/index.ts

@@ -0,0 +1,11 @@
+export enum RoutePath {
+  login = '/login/:id',
+
+  home = '/',
+
+  scene = '/scene'
+}
+
+export * from './scene'
+export * from './thunk'
+export * from './api'

+ 33 - 0
src/constant/scene.ts

@@ -0,0 +1,33 @@
+export enum SceneType {
+  SWKK,
+  SWKJ,
+  SWSS,
+  QJKK
+}
+
+export const SceneTypeDesc: { [key in SceneType]: string }  = {
+  [SceneType.SWKK]: '四维看看',
+  [SceneType.SWKJ]: '四维看见',
+  [SceneType.SWSS]: '四维深时',
+  [SceneType.QJKK]: '全景看看',
+}
+
+
+
+export enum SceneStatus {
+  DEL = -1,
+  RUN = 0,
+  ERR = 1,
+  SUCCESS = 2,
+  ARCHIVE = 3,
+  RERUN = 4,
+}
+
+export const SceneStatusDesc: { [key in SceneStatus]: string }  = {
+  [SceneStatus.DEL]: '场景被删',
+  [SceneStatus.RUN]: '计算中',
+  [SceneStatus.ERR]: '计算失败',
+  [SceneStatus.SUCCESS]: '计算成功',
+  [SceneStatus.ARCHIVE]: '封存',
+  [SceneStatus.RERUN]: '重新计算中',
+}

+ 14 - 0
src/constant/thunk.ts

@@ -0,0 +1,14 @@
+
+export enum ThunkStatus {
+  idle = 0,
+  loading = 1,
+  success,
+  err
+}
+
+export const ThunkStatusDesc: { [key in ThunkStatus]: string } = {
+  [ThunkStatus.idle]: '未请求',
+  [ThunkStatus.loading]: '请求中',
+  [ThunkStatus.success]: '请求成功',
+  [ThunkStatus.err]: '请求错误',
+}

+ 25 - 0
src/hook/index.ts

@@ -0,0 +1,25 @@
+import { useState, useEffect } from 'react'
+
+export const useStoreState = <T>(
+  key: string,
+  initialValue?: T, 
+  convert?: (data: string) => T,
+  store: Storage = localStorage
+) => {
+  const storeVal = store.getItem(key)
+  if (storeVal) {
+    initialValue = convert ? convert(storeVal) : JSON.parse(storeVal)
+  }
+
+  const [value, setValue] = useState<T>(initialValue as T)
+
+  useEffect(() => {
+    store.setItem(key, JSON.stringify(value))
+  }, [store, key, value])
+
+  return [
+    value, 
+    setValue,
+    () => store.removeItem(key)
+  ] as const
+}

+ 0 - 13
src/index.css

@@ -1,13 +0,0 @@
-body {
-  margin: 0;
-  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
-    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
-    sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-}
-
-code {
-  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
-    monospace;
-}

+ 7 - 9
src/index.tsx

@@ -1,19 +1,17 @@
 import React from 'react';
 import ReactDOM from 'react-dom/client';
-import './index.css';
-import App from './App';
-import reportWebVitals from './reportWebVitals';
+import { AppRouter } from 'router'
+import { AppStore } from 'store'
+import './public.scss'
 
 const root = ReactDOM.createRoot(
   document.getElementById('root') as HTMLElement
 );
+
 root.render(
   <React.StrictMode>
-    <App />
+    <AppStore>
+      <AppRouter />
+    </AppStore>
   </React.StrictMode>
 );
-
-// If you want to start measuring performance in your app, pass a function
-// to log results (for example: reportWebVitals(console.log))
-// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
-reportWebVitals();

+ 0 - 0
src/layout/example/index.tsx


+ 15 - 0
src/layout/header/index.tsx

@@ -0,0 +1,15 @@
+import { Avatar } from 'antd'
+import style from './style.module.scss'
+
+
+export const HeaderContent = () => {
+  return (
+    <>
+      <h2 className={style.title}>三维数据融合平台</h2>
+      <div className={style.avatar}>
+        <Avatar src="https://joeschmoe.io/api/v1/random" />
+        <span className={style.username}>用户</span>
+      </div>
+    </>
+  )
+}

+ 16 - 0
src/layout/header/style.module.scss

@@ -0,0 +1,16 @@
+.title {
+  font-size: 24px;
+  font-weight: bold;
+  color: #FFFFFF;
+  margin: 0;
+}
+
+.avatar {
+  color: #fff;
+  display: flex;
+  align-items: center;
+
+  .username {
+    margin-left: 10px;
+  }
+}

+ 40 - 0
src/layout/top/index.tsx

@@ -0,0 +1,40 @@
+import { Layout } from 'antd'
+import { Outlet, useRoute, useNavigate, RouteConfigTree } from 'router'
+import { RouteMenu } from 'components'
+import { HeaderContent } from '../header'
+
+import style from './style.module.scss'
+
+const { Header, Sider, Content  } = Layout
+
+export const Top = () => {
+  const route = useRoute()
+  const navigate = useNavigate()
+  const onSelect = (selectRoute: RouteConfigTree) => {
+    selectRoute.path !== route?.path && navigate(selectRoute.path)
+  }
+
+  const renderMenu = route?.parent?.children 
+    && <RouteMenu 
+        routes={route?.parent?.children} 
+        onSelect={onSelect} 
+      />
+
+  return (
+    <Layout>
+      <Header className={style.header}>
+        <HeaderContent />
+      </Header>
+      <Layout>
+        <Sider className={style.sider}>
+          { renderMenu }
+        </Sider>
+        <Content>
+          <Outlet />
+        </Content>
+      </Layout>
+    </Layout>
+  )
+}
+
+export default Top

+ 12 - 0
src/layout/top/style.module.scss

@@ -0,0 +1,12 @@
+.sider {
+  background: #FFFFFF;
+  width: 208px;
+  box-shadow: 0px 2px 8px 0px rgba(0,0,0,0.1500);
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 20px;
+}

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 1
src/logo.svg


+ 11 - 0
src/public.scss

@@ -0,0 +1,11 @@
+@import '~antd/dist/antd.css';
+
+html, body, #root {
+  width: 100%;
+  height: 100%;
+}
+
+#root {
+  display: flex;
+  flex-direction: column;
+}

+ 22 - 0
src/react-app-env.d.ts

@@ -1 +1,23 @@
 /// <reference types="react-scripts" />
+
+type ExcludeObject<T, U> = {
+  [key in keyof T]: Exclude<T[key], U>
+}
+type SetObject<T, K, U> = {
+  [key in keyof T]: key extends K ? U : T[key]
+}
+
+type ExtractRouteParamsKey<T extends string> = 
+  T extends `${infer P1}:${infer P2}/${infer P3}`
+    ? P2 | ExtractRouteParamsKey<P3>
+    : T extends `${infer P1}:${infer P2}`
+      ? P2
+  : T extends `:${infer P2}/${infer P3}`
+    ? P2 | ExtractRouteParamsKey<P3>
+    : T extends `:${infer P2}`
+      ? P2
+      : never
+
+type ExtractRouteParams<T> = {
+  [key in ExtractRouteParamsKey<T>]: string
+}

+ 0 - 15
src/reportWebVitals.ts

@@ -1,15 +0,0 @@
-import { ReportHandler } from 'web-vitals';
-
-const reportWebVitals = (onPerfEntry?: ReportHandler) => {
-  if (onPerfEntry && onPerfEntry instanceof Function) {
-    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
-      getCLS(onPerfEntry);
-      getFID(onPerfEntry);
-      getFCP(onPerfEntry);
-      getLCP(onPerfEntry);
-      getTTFB(onPerfEntry);
-    });
-  }
-};
-
-export default reportWebVitals;

+ 68 - 0
src/router/config.ts

@@ -0,0 +1,68 @@
+import { RoutePath } from 'constant'
+import { useLocation } from 'react-router-dom'
+import { verifiRoutePath } from 'utils'
+import { UpSquareOutlined } from '@ant-design/icons'
+
+import type { ComponentType, FunctionComponent } from 'react'
+
+export type RouteConfig = { 
+  path:  RoutePath,
+  element: () => Promise<{ default: ComponentType<any> }>,
+  children?: RoutesConfig,
+  index?: boolean,
+  meta?: {
+    icon: FunctionComponent,
+    label: string
+  }
+}
+
+export type RoutesConfig = RouteConfig[]
+
+export const routesConfig: RoutesConfig = [
+  {
+    path: RoutePath.login,
+    element: () => import('views/login'),
+  },
+  {
+    path: RoutePath.home,
+    element: () => import('layout/top'),
+    children: [
+      {
+        path: RoutePath.scene,
+        element: () => import('views/scene'),
+        meta: {
+          label: '场景管理',
+          icon: UpSquareOutlined
+        }
+      }
+    ]
+  },
+]
+
+
+
+export type RouteConfigTree = RouteConfig & { parent: RouteConfig | null }
+
+export const findRouteByPathName = (
+  pathname: string, 
+  routes: RoutesConfig = routesConfig, 
+): RouteConfigTree | undefined => {
+  for (const route of routes) {
+    if (verifiRoutePath(pathname, route.path)) {
+      return { ...route, parent: null }
+    } else if (route.children?.length) {
+      const matchRoute = findRouteByPathName(pathname, route.children)
+      if (matchRoute) {
+        return { ...matchRoute, parent: route }
+      }
+    }
+  }
+}
+
+export const useRoute = () => {
+  const { pathname } = useLocation()
+  return findRouteByPathName(pathname)
+}
+
+export { RoutePath }
+export { fillRoutePath } from 'utils'

+ 30 - 0
src/router/index.tsx

@@ -0,0 +1,30 @@
+import { HashRouter as Router, useRoutes } from 'react-router-dom'
+import { lazy } from 'react'
+import { AsyncComponent } from 'components'
+import { routesConfig } from './config'
+
+import type { RouteObject } from 'react-router-dom'
+import type { RoutesConfig } from './config'
+
+export const getReactRouteConfig = (config: RoutesConfig): RouteObject[] => {
+  return config.map(route => ({
+    ...route,
+    element: <AsyncComponent mount={lazy(route.element)} route={route} />,
+    children: route.children ? getReactRouteConfig(route.children) : []
+  }))
+}
+
+export const GetRoutes = () => useRoutes(getReactRouteConfig(routesConfig))
+
+export const AppRouter = () => (
+  <Router>
+    <GetRoutes />
+  </Router>
+)
+
+export * from './config'
+export {
+  useNavigate,
+  useHref,
+  Outlet
+} from 'react-router-dom'

+ 0 - 5
src/setupTests.ts

@@ -1,5 +0,0 @@
-// jest-dom adds custom jest matchers for asserting on DOM nodes.
-// allows you to do things like:
-// expect(element).toHaveTextContent(/react/i)
-// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom';

+ 68 - 0
src/store/help.ts

@@ -0,0 +1,68 @@
+import { createSelector } from '@reduxjs/toolkit'
+import { ThunkStatus } from 'constant'
+import { filterItems, findItems } from 'utils'
+
+import type { ActionReducerMapBuilder, AsyncThunk } from '@reduxjs/toolkit'
+
+type BaseThunkState = {
+  status: ThunkStatus
+  error: null | string
+}
+export type ThunkState<T> = BaseThunkState & T
+
+export const initialThukState = {
+  status: ThunkStatus.idle,
+  error: null
+}
+
+
+type GetState<State, T> = (state: State) => T
+type ArraySelects<State, T extends any[]> = {
+  listSelector: (state: State) => T,
+  filterSelector: (state: State, search: Partial<T[number]>) => T,
+  findSelector: (state: State, search: Partial<T[number]>) => T[number]
+}
+type KeyArraySelects<State, T extends any[], K extends keyof T[number]> = ArraySelects<State, T> & {
+  filterSelector: (state: State, key: T[number][K]) => T, 
+  findSelector: (state: State, key: T[number][K]) => T[number]
+}
+
+export function createArrayThunkSelects<State, T extends any[]>(getState: GetState<State, T>): ArraySelects<State, T>
+export function createArrayThunkSelects<State, T extends any[], K extends keyof T[number]>(getState: GetState<State, T>, key: K): KeyArraySelects<State, T, K>
+export function createArrayThunkSelects<State, T extends any[], K extends keyof T[number]>(
+  getState: GetState<State, T>,
+  key?: K
+): ArraySelects<State, T> | KeyArraySelects<State, T, K> {
+  const listSelector = (state: State) => getState(state)
+  const filterSelector: any = createSelector(
+    [listSelector, (state: State, search) => search],
+    (list, search) => filterItems(list, search, key)
+  )
+  const findSelector: any = createSelector(
+    [listSelector, (state: State, search) => search],
+    (list, search) => findItems(list, search, key)
+  )
+  
+  return {
+    listSelector,
+    filterSelector,
+    findSelector
+  }
+}
+
+export const thunkStatusAutoSet = <K, T extends ThunkState<K>, Thunk extends AsyncThunk<any, any, any>>(
+  builder: ActionReducerMapBuilder<T>,
+  thunk: Thunk
+) => {
+  builder
+    .addCase(thunk.pending, state => {
+      state.status = ThunkStatus.loading
+    })
+    .addCase(thunk.fulfilled, (state, action) => {
+      state.status = ThunkStatus.success
+    })
+    .addCase(thunk.rejected, (state, action) => {
+      state.status = ThunkStatus.err
+    })
+  return builder
+}

+ 27 - 0
src/store/index.tsx

@@ -0,0 +1,27 @@
+import { configureStore } from '@reduxjs/toolkit'
+import { Provider } from 'react-redux'
+import { useDispatch as useDispatchRaw, useSelector as useSelectorRaw } from 'react-redux'
+import { sceneReducer } from './scene'
+
+import type { TypedUseSelectorHook } from 'react-redux'
+
+export const store = configureStore({
+  reducer: {
+    scene: sceneReducer
+  }
+})
+
+export type StoreState = ReturnType<typeof store.getState>
+export type AppDispatch = typeof store.dispatch
+export type AppSelector = TypedUseSelectorHook<StoreState>
+
+export const useDispatch = useDispatchRaw<AppDispatch>
+export const useSelector: AppSelector = useSelectorRaw
+
+export const AppStore = ({ children }: { children: any }) => (
+  <Provider store={store}>
+    { children }
+  </Provider>
+)
+
+export default store

+ 42 - 0
src/store/scene.ts

@@ -0,0 +1,42 @@
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
+import { getSceneByType } from 'api'
+import { initialThukState, createArrayThunkSelects, thunkStatusAutoSet } from './help'
+
+import type { ThunkState } from './help'
+import type { Scenes } from 'api'
+import type { StoreState } from './'
+
+export type SceneState = ThunkState<{value: Scenes}>
+
+const initialState: SceneState = {
+  ...initialThukState,
+  value: []
+}
+
+const sceneSlice = createSlice({
+  name: 'scene',
+  initialState,
+  reducers: {
+
+  },
+  extraReducers(builder) {
+    thunkStatusAutoSet(builder, fetchScenes)
+
+    builder
+      .addCase(fetchScenes.fulfilled, (state, action) => {
+        state.value = action.payload
+      })
+  }
+})
+
+export const sceneReducer = sceneSlice.reducer
+export const {
+  listSelector: scenesSelector,
+  findSelector: sceneSelector,
+  filterSelector: filterScenesSelector
+} = createArrayThunkSelects(
+  (state: StoreState) => state.scene.value,
+  'id'
+)
+
+export const fetchScenes = createAsyncThunk('fetch/scenes', getSceneByType)

+ 50 - 0
src/utils/array.ts

@@ -0,0 +1,50 @@
+import { isObject } from './base'
+
+
+const normFindArgs = <T, K extends keyof T, S extends boolean>(
+  search: T, 
+  single: S,
+  key?: K, 
+  relation: 'and' | 'or' = 'and'
+): { filter: 'every' | 'some', api: S extends true ? 'find' : 'filter', condition: any } => {
+  if (!isObject(search) || Object.keys(search).length === 0) {
+    search = { [key as string]: search } as any
+  }
+  return {
+    filter: relation === 'and' ? 'every' : 'some',
+    api: (single ? 'find' : 'filter') as any,
+    condition: search as T
+  }
+}
+
+export const filterItems = <T, K extends keyof T>(
+  items: T[], 
+  search: Partial<T> | T[K], 
+  key?: K,
+  relation?: 'and' | 'or'
+): T[] => {
+  const {
+    filter,
+    api,
+    condition
+  } = normFindArgs(search, false, key as any, relation)
+
+  const keys = Object.keys(condition)
+  return items[api](item => keys[filter](key => (item as any)[key] === condition[key]))
+}
+
+
+export const findItems = <T, K extends keyof T>(
+  items: T[], 
+  search: Partial<T> | T[K], 
+  key?: K,
+  relation?: 'and' | 'or'
+): T | undefined => {
+  const {
+    filter,
+    api,
+    condition
+  } = normFindArgs(search, true, key as any, relation)
+  const keys = Object.keys(condition)
+  return items[api](item => keys[filter](key => (item as any)[key] === condition[key]))
+}

+ 3 - 0
src/utils/base.ts

@@ -0,0 +1,3 @@
+
+export const rawType = (data: any) => Object.prototype.toString.call(data).slice(8, -1)
+export const isObject = (data: any) => rawType(data) === 'Object'

+ 3 - 0
src/utils/index.ts

@@ -0,0 +1,3 @@
+export * from './array'
+export * from './base'
+export * from './route'

+ 20 - 0
src/utils/route.ts

@@ -0,0 +1,20 @@
+
+export const fillRoutePath = <T extends string>(path: T, params: ExtractRouteParams<T>) => {
+  let processPath: string = path
+
+  for (const [key, value] of Object.entries(params)) {
+    const rg = new RegExp(`:${key}/?`)
+    processPath = processPath.replace(rg, value as string)
+  }
+
+  return processPath
+}
+
+const place = /(?:\/:([^/]*))/g
+export const verifiRoutePath = (pathname: string, path: string) => {
+  const rg = path
+    .replace(/\/([^:])/g, (_, d) => `\\/${d}`)
+    .replace(place, () => `(?:/[^/]*)`)
+  return new RegExp(`^${rg}$`).test(pathname)
+}
+

+ 10 - 0
src/views/login/index.tsx

@@ -0,0 +1,10 @@
+import { useRoute, RoutesConfig } from 'router'
+
+export const Login = ({route}: {route: RoutesConfig}) => {
+  console.log(route)
+  const match = useRoute()
+  console.log(match)
+  return <div>123</div>
+}
+
+export default Login

+ 25 - 0
src/views/scene/index.tsx

@@ -0,0 +1,25 @@
+import { SceneType, SceneTypeDesc } from 'constant'
+import { useStoreState } from 'hook'
+import { Tabs } from 'components'
+import { SceneList } from './list'
+
+const ScenePage = () => {
+  const [type, setType] = useStoreState(
+    'scene-page-type', 
+    SceneType.SWSS,
+    str => Number(str) as SceneType
+  )
+  const tabItems = Object.entries(SceneTypeDesc)
+    .map(([key, val]) => [Number(key) as SceneType, val] as const)
+
+
+  return (
+    <Tabs
+      items={tabItems} 
+      active={type} 
+      onChange={type => setType(Number(type))} 
+      content={ <SceneList type={type} /> } />
+  )
+}
+
+export default ScenePage

+ 34 - 0
src/views/scene/list.tsx

@@ -0,0 +1,34 @@
+import { SceneType } from "constant"
+import { Table } from 'antd'
+import style from './style.module.scss'
+import { getTypeColumns } from './table-cloumns'
+
+
+interface DataType {
+  title: string
+  sncode: string,
+  time: string,
+  status: number,
+}
+
+const datas: DataType[] = [
+  {
+    title: '123123',
+    sncode: '123123',
+    time: '123123',
+    status: 2,
+  }
+]
+
+
+export type SceneListProps = {type: SceneType}
+
+export const SceneList = ({ type }: SceneListProps) => {
+  return (
+    <div className={style['table-body']}>
+      <Table columns={getTypeColumns(type)} dataSource={datas} rowKey="title" />
+    </div>
+  )
+}
+
+export default SceneList

+ 3 - 0
src/views/scene/style.module.scss

@@ -0,0 +1,3 @@
+.table-body {
+  padding: 0 24px;
+}

+ 66 - 0
src/views/scene/table-cloumns.tsx

@@ -0,0 +1,66 @@
+import { Space } from 'antd'
+import { SceneType } from 'constant';
+
+import type { ColumnsType } from 'antd/es/table';
+
+interface DataType {
+  title: string
+  sncode: string,
+  time: string,
+  status: number,
+}
+
+type Column = ColumnsType<DataType>[number]
+
+export const titleColumn: Column = {
+  title: '名称',
+  dataIndex: 'title',
+  key: 'title',
+}
+export const sncodeColumn: Column = {
+  title: 'SN码',
+  dataIndex: 'sncode',
+  key: 'sncode',
+}
+export const timeColumn: Column = {
+  title: '拍摄时间',
+  dataIndex: 'time',
+  key: 'time',
+}
+export const statusColumn: Column = {
+  title: '状态',
+  dataIndex: 'status',
+  key: 'status',
+}
+export const actionColumn: Column = {
+  title: '操作',
+  key: 'action',
+  render: (_, record) => (
+    <Space size="middle">
+      <span>Invite {record.title}</span>
+      <span>Delete</span>
+    </Space>
+  )
+}
+
+export const getTypeColumns = (type: SceneType): Column[] => {
+  switch(type) {
+    case SceneType.SWKK:
+      return [
+        sncodeColumn,
+        timeColumn,
+        statusColumn,
+        actionColumn
+      ]
+    case SceneType.QJKK:
+    case SceneType.SWKJ:
+    case SceneType.SWSS:
+      return [
+        titleColumn,
+        sncodeColumn,
+        timeColumn,
+        statusColumn,
+        actionColumn
+      ]
+  }
+}

+ 3 - 2
tsconfig.json

@@ -16,9 +16,10 @@
     "module": "esnext",
     "moduleResolution": "node",
     "resolveJsonModule": true,
-    "isolatedModules": true,
+    "isolatedModules": false,
     "noEmit": true,
-    "jsx": "react-jsx"
+    "jsx": "react-jsx",
+    "baseUrl": "src"
   },
   "include": [
     "src"

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 9742 - 0
yarn.lock