Browse Source

feat: 制作场景管理

bill 3 năm trước cách đây
mục cha
commit
e30cc7f3e8

+ 2 - 0
package.json

@@ -17,7 +17,9 @@
     "axios": "^0.27.2",
     "canvas-nest.js": "^2.0.4",
     "craco-less": "^2.0.0",
+    "js-base64": "^3.7.2",
     "lodash": "^4.17.21",
+    "mitt": "^3.0.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-redux": "^8.0.2",

+ 18 - 0
src/api/index.ts

@@ -1,6 +1,24 @@
 
 
 export { ResCode } from 'constant'
+export type { ResData } from './setup'
+
+export type BasePagingRequest = {
+  pageNum: number,
+  pageSize: number,
+}
+
+export type ParamsPagingRequest = {[key in string]: any}
+
+export type PagingRequest<T extends ParamsPagingRequest> = BasePagingRequest & T
+
+export type PagingResult<T> = {
+  list: T,
+  total: number,
+  pageNum: number,
+  pageSize: number
+}
+
 export * from './scene'
 export * from './instance'
 export * from './user'

+ 12 - 6
src/api/instance.ts

@@ -1,32 +1,38 @@
 import { axiosFactory } from './setup'
 import { message } from 'antd'
-import { LOGIN } from './url'
+import { LOGIN, ResCodeDesc } from 'constant'
 
 const instance = axiosFactory()
 
 export const {
   axios,
-  addReqErrorHandler,
   addUnsetTokenURLS,
+  addReqErrorHandler,
   addResErrorHandler,
+  delReqErrorHandler,
+  delResErrorHandler,
+  getToken,
   setToken,
   delToken,
   setDefaultURI
 } = instance
 
-addReqErrorHandler(err => message.error(err.message))
+addReqErrorHandler(err => {
+  message.error(err.message)
+})
 
 addResErrorHandler(
   (response, data) => {
     if (response.status !== 200) {
       message.error(response.statusText)
-    } else {
-      message.error(data?.message)
+    } else if (data) {
+      const msg = data.code && ResCodeDesc[data.code] ? ResCodeDesc[data.code] : data?.message
+      message.error(msg)
     }
   }
 )
 
-addUnsetTokenURLS(LOGIN, '/')
+addUnsetTokenURLS(LOGIN)
 setDefaultURI('/api')
 
 export default axios

+ 83 - 9
src/api/scene.ts

@@ -1,18 +1,92 @@
-import { SceneStatus, SceneType } from 'constant'
 import axios from './instance'
+import { 
+  jsonToForm, 
+  uploadProgressFactory, 
+  UploadProgressCallback 
+} from 'utils'
+import { 
+  QuoteSceneStatus, 
+  ModelSceneStatus, 
+  SceneType,
 
+  UPLOAD_HEADS,
 
-export interface Scene {
+  SCENE_LIST, 
+  MODEL_SCENE_LIST, 
+  UPLOAD_MODEL,
+  DELETE_MODEL
+} from 'constant'
+
+
+import type { PagingResult, PagingRequest } from '.'
+
+export interface BaseScene {
   id: string,
   title: string
-  sncode: string,
-  time: string,
-  status: SceneStatus,
+  time: string
+}
+
+export interface QuoteScene extends BaseScene {
+  type: SceneType.SWKJ | SceneType.SWKK | SceneType.SWSS,
+  status: QuoteSceneStatus,
+  snCode: string,
+  num: string
+}
+
+export interface ModelScene extends BaseScene {
+  status: ModelSceneStatus,
+  type: SceneType.SWMX,
+  rawType: string,
+  progress?: number
+} 
+
+export type Scene = QuoteScene | ModelScene
+
+export interface ServeModelScene {
+  modelId:	string	
+  modelTitle:	string	
+  modelDateType:	string	
+  modelSize:	string	
+  modelObjUrl:	string	
+  modelGlbUrl:	string	
+  createStatus:	ModelSceneStatus	
+  createTime:	string	
 }
 
 export type Scenes = Scene[]
 
-export const getSceneByType = async (type: SceneType, keyword?: string) => {
-  const scenes = await axios.get<Scenes>('/scenes', { params: { keyword } })
-  return scenes
-}
+// 获取场景列表
+export type GetSceneByTypeParams = PagingRequest<{type: SceneType, sceneName?: string}>
+export const getSceneByType = async (data: GetSceneByTypeParams): Promise<PagingResult<Scene[]>> => {
+  if (data.type === SceneType.SWMX) {
+    const modelScenes = await axios.post<PagingResult<ServeModelScene[]>>(MODEL_SCENE_LIST, data)
+    return {
+      ...modelScenes,
+      list: modelScenes.list.map(scene => ({
+        id: scene.modelId,
+        title: scene.modelTitle,
+        time: scene.createTime,
+        status: scene.createStatus,
+        type: SceneType.SWMX,
+        rawType: scene.modelDateType,
+      }))
+    }
+  } else {
+    return await axios.post<PagingResult<Scenes>>(SCENE_LIST, data)
+  }
+}
+
+// 上传模型文件
+export type UploadModelSceneProps = { file: File, progressCallback?: UploadProgressCallback }
+export const uploadModelScene = ({ file, progressCallback }: UploadModelSceneProps) => 
+  axios<undefined>({
+    method: 'POST',
+    url: UPLOAD_MODEL, 
+    data: jsonToForm({ file }), 
+    headers: UPLOAD_HEADS,
+    onUploadProgress: progressCallback && uploadProgressFactory(progressCallback)
+  })
+
+// 删除模型文件
+export const deleteModelScene = (modelId: ServeModelScene['modelId']) => 
+  axios.post<undefined>(DELETE_MODEL, { modelId })

+ 29 - 14
src/api/setup.ts

@@ -19,6 +19,7 @@ export const axiosFactory = () => {
   }
 
 
+
   type AxiosConfig = typeof axiosConfig
   type ExponseApi<K extends keyof AxiosConfig> = 
     { set: (val: AxiosConfig[K]) => void } 
@@ -30,22 +31,22 @@ export const axiosFactory = () => {
         : { del: () => void })
 
   const getExponseApi = <K extends keyof AxiosConfig>(key: K): ExponseApi<K> => {
-    const axiosObj: any = axiosConfig[key]
+    let axiosObj = axiosConfig[key] as any[]
     const apis: any = { 
       set (val: AxiosConfig[K]) {
-        axiosConfig[key] = val
+        axiosObj = axiosConfig[key] = val as any
       }
     }
     
     if (Array.isArray(axiosObj)) {
       apis.add = (...val: any[]) => {
-        axiosObj.push(...val as any)
+        axiosObj.push(...val)
       }
       apis.del = (...val: any[]) => {
         if (val) {
-          axiosConfig[key] = axiosObj.filter(item => !val?.includes(item)) as any
+          apis.set(axiosObj.filter((item: any) => !val?.includes(item)) as any)
         } else {
-          axiosConfig[key] = [] as any  
+          axiosObj.length = 0
         }
       }
     } else {
@@ -56,11 +57,16 @@ export const axiosFactory = () => {
     return apis
   }
 
-  const {
-    set: setToken,
-    del: delToken,
-  } = getExponseApi('token')
-
+  const getToken = () => axiosConfig.token
+  const setToken = (token: string) => {
+    localStorage.setItem('token', token)
+    axiosConfig.token = token
+  }
+  const delToken = () => {
+    localStorage.removeItem('token')
+    axiosConfig.token = null
+  }
+  
   const {
     set: setUnsetTokenURLS,
     add: addUnsetTokenURLS,
@@ -98,13 +104,21 @@ export const axiosFactory = () => {
   const matchURL = (urls: string[], config: AxiosRequestConfig<any>) => 
     config.url && urls.includes(config.url)
 
+  const callErrorHandler = (key: 'req' | 'res', ...args: any[]) => {
+    Promise.resolve()
+      .then(() => {
+        const api = `${key}ErrorHandler`
+        ;(axiosConfig as any)[api].forEach((handler: any) => handler(...args))
+      })
+  }
+
   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))
+            callErrorHandler('req', error, config)
             throw error
           }
         } else {
@@ -125,10 +139,10 @@ export const axiosFactory = () => {
       }
 
       if (response.status !== 200) {
-        axiosConfig.resErrorHandler.forEach(handler => handler(response))
+        callErrorHandler('res', response)
         throw new Error(response.statusText)
       } else if (response.data.code !== ResCode.SUCCESS) {
-        axiosConfig.resErrorHandler.forEach(handler => handler(response, response.data))
+        callErrorHandler('res', response, response.data)
         if (response.data.code === ResCode.TOKEN_INVALID) {
           delToken()
         }
@@ -139,7 +153,7 @@ export const axiosFactory = () => {
     },
     (err) => {
       if (!matchURL(axiosConfig.unResErrorSet, err.config)) {
-        axiosConfig.resErrorHandler.forEach(handler => handler(err.response))
+        callErrorHandler('res', err.response)
       }
       throw new Error(err.response.statusText)
     }
@@ -170,6 +184,7 @@ export const axiosFactory = () => {
 
   return {
     axios,
+    getToken,
     setToken,
     delToken,
     setUnsetTokenURLS,

+ 0 - 1
src/api/url.ts

@@ -1 +0,0 @@
-export const LOGIN = `/fusion/fdLogin`

+ 7 - 3
src/api/user.ts

@@ -1,9 +1,11 @@
 import { axios, setToken } from './instance'
-import { LOGIN } from './url'
+import { LOGIN } from 'constant'
+import { encodePwd } from 'utils'
 
 export type User = {
   id: string,
   userName: string,
+  nickName: string,
   head: string,
   cameraCount: number
 }
@@ -11,8 +13,10 @@ export type User = {
 // 登录
 export type LoginParams = { phoneNum: string, password: string}
 export const login = async (data: LoginParams) => {
-  axios.get('/')
-  const { token, user } = await axios.post<{token: string, user: User}>(LOGIN, data)
+  const { token, user } = await axios.post<{token: string, user: User}>(
+    LOGIN, 
+    { ...data, password: encodePwd(data.password) }
+  )
   setToken(token)
   return user
 }

+ 3 - 3
src/components/tabs/index.tsx

@@ -8,14 +8,14 @@ const { TabPane: ATabPane } = ATabs
 export type TabsProps<T> = { 
   onChange: (type: string) => void, 
   active: T,
-  content: ReactNode,
+  renderContent: (tab: T) => ReactNode,
   items: Array<readonly [T, string]>
 }
 
-export const Tabs = <T extends Key>({ onChange, active, content, items }: TabsProps<T>) => {
+export const Tabs = <T extends Key>({ onChange, active, renderContent, items }: TabsProps<T>) => {
   const renderOptions =items.map(([key, value]) => (
       <ATabPane tab={value} key={key} className={style['tab-panel']}>
-        {content}
+        {renderContent(key)}
       </ATabPane>
     ))
 

+ 18 - 2
src/constant/api.ts

@@ -1,9 +1,25 @@
 export enum ResCode {
-  TOKEN_INVALID = 403,
+  TOKEN_INVALID = 4008,
   SUCCESS = 0
 }
 
 export const ResCodeDesc: { [key in ResCode]: string } = {
   [ResCode.TOKEN_INVALID]: 'token已失效',
   [ResCode.SUCCESS]: '请求成功'
-}
+}
+
+export const UPLOAD_HEADS = {
+  'Content-Type': 'multipart/form-data'
+}
+
+
+export const LOGIN = `/fusion/fdLogin`
+
+// 场景列表
+export const SCENE_LIST = `/fusion/scene/list`
+
+
+// 三维模型
+export const MODEL_SCENE_LIST = `/fusion/model/list`
+export const UPLOAD_MODEL = `/fusion/model/uploadObj`
+export const DELETE_MODEL = `/fusion/model/delete`

+ 4 - 3
src/constant/route.ts

@@ -1,7 +1,8 @@
 export enum RoutePath {
   login = '/login',
-
   home = '/',
+  scene = '/scene',
+  example = '/example',
+}
 
-  scene = '/scene'
-}
+export const ViewHome = RoutePath.scene

+ 40 - 11
src/constant/scene.ts

@@ -2,19 +2,34 @@ export enum SceneType {
   SWKK,
   SWKJ,
   SWSS,
-  QJKK
+  SWMX,
+  // QJKK
 }
 
 export const SceneTypeDesc: { [key in SceneType]: string }  = {
   [SceneType.SWKK]: '四维看看',
   [SceneType.SWKJ]: '四维看见',
   [SceneType.SWSS]: '四维深时',
-  [SceneType.QJKK]: '全景看看',
+  [SceneType.SWMX]: '三维模型',
+  // [SceneType.QJKK]: '全景看看',
 }
 
+export const SceneTypeDomain: { [key in SceneType]: string } = {
+  [SceneType.SWKK]: 'https://test.4dkankan.com',
+  [SceneType.SWKJ]: 'https://test.4dkankan.com',
+  [SceneType.SWSS]: 'https://uat-laser.4dkankan.com/uat',
+  [SceneType.SWMX]: 'https://uat-laser.4dkankan.com/uat',
+}
+
+export const SceneTypePaths: { [key in SceneType]: string[] } = {
+  [SceneType.SWKK]: ['spc.html', 'epc.html'],
+  [SceneType.SWKJ]: ['spc.html', 'epc.html'],
+  [SceneType.SWSS]: ['index.html'],
+  [SceneType.SWMX]: ['index.html'],
+}
 
 
-export enum SceneStatus {
+export enum QuoteSceneStatus {
   DEL = -1,
   RUN = 0,
   ERR = 1,
@@ -23,11 +38,25 @@ export enum SceneStatus {
   RERUN = 4,
 }
 
-export const SceneStatusDesc: { [key in SceneStatus]: string }  = {
-  [SceneStatus.DEL]: '场景被删',
-  [SceneStatus.RUN]: '计算中',
-  [SceneStatus.ERR]: '计算失败',
-  [SceneStatus.SUCCESS]: '计算成功',
-  [SceneStatus.ARCHIVE]: '封存',
-  [SceneStatus.RERUN]: '重新计算中',
-}
+export const QuoteSceneStatusDesc: { [key in QuoteSceneStatus]: string }  = {
+  [QuoteSceneStatus.DEL]: '场景被删',
+  [QuoteSceneStatus.RUN]: '计算中',
+  [QuoteSceneStatus.ERR]: '计算失败',
+  [QuoteSceneStatus.SUCCESS]: '计算成功',
+  [QuoteSceneStatus.ARCHIVE]: '封存',
+  [QuoteSceneStatus.RERUN]: '重新计算中',
+}
+
+
+export enum ModelSceneStatus {
+  ERR = -1,
+  RUN = 0,
+  SUCCESS = 1,
+}
+
+
+export const ModelSceneStatusDesc: { [key in ModelSceneStatus]: string }  = {
+  [ModelSceneStatus.ERR]: '失败',
+  [ModelSceneStatus.RUN]: '上传中',
+  [ModelSceneStatus.SUCCESS]: '成功',
+}

+ 15 - 2
src/hook/index.ts

@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react'
+import { useState, useEffect, useCallback } from 'react'
 
 export const useStoreState = <T>(
   key: string,
@@ -22,4 +22,17 @@ export const useStoreState = <T>(
     setValue,
     () => store.removeItem(key)
   ] as const
-}
+}
+
+export const useLoading = () => {
+  const [loading, setLoading] = useState(false)
+  const setPromise = useCallback(<T>(promise: Promise<T>): Promise<T> => {
+    setLoading(true)
+    promise.finally(() => setLoading(false))
+    return promise
+  }, [setLoading])
+
+  return [loading, setPromise] as const
+}
+
+export * from './paging'

+ 72 - 0
src/hook/paging.ts

@@ -0,0 +1,72 @@
+import { useCallback, useEffect, useState } from "react"
+import { useDispatch } from 'react-redux'
+import { quickSetState, QuickSetState } from 'utils'
+
+import type { Dispatch, SetStateAction } from 'react'
+import type { AsyncThunk } from '@reduxjs/toolkit'
+import type { 
+  PagingResult, 
+  PagingRequest, 
+  BasePagingRequest, 
+  ParamsPagingRequest
+} from 'api'
+
+
+export type Paging = BasePagingRequest & { total?: PagingResult<any>['total'] }
+export type GetPagingDataParams<T extends any[]> = [Paging, ...T]
+export type GetPagingData<T extends any[], R> = (...args: GetPagingDataParams<T>) => Promise<PagingResult<R>>
+export type UsePagingStatesResult<T extends any[], R = [Paging, ...T]> = {
+  [key in keyof R]: [R[key], ReturnType<QuickSetState<Dispatch<SetStateAction<R[key]>>>>]
+}
+export type UsePagingResult<T extends any[]> = [...UsePagingStatesResult<T>, () => void]
+
+export const usePaging = <T extends any[], R>(
+  getData: GetPagingData<T, R>,
+  initPaging: Paging, 
+  ...params: T
+) => {
+  const [paging, setPaging] = useState(initPaging)
+  const states = params.map(useState)
+  const surplusParams = states.map(state => state[0])
+  const refresh = () => {
+    const allParams = [paging, ...surplusParams] as GetPagingDataParams<T>
+    getData(...allParams).then(data => {
+      if (paging.total !== data.total) {
+        setPaging(paging => ({ ...paging, total: data.total }))
+      }
+    })
+  }
+
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+  useEffect(refresh, [...surplusParams, paging.pageNum, paging.pageSize])
+
+  return [
+    [paging, quickSetState(setPaging)],
+    ...states.map(state => ([state[0], quickSetState(state[1])])),
+    refresh
+  ] as unknown as UsePagingResult<T>
+}
+
+
+
+export const useThunkPaging = <T extends ParamsPagingRequest, R, U>(
+  initParams: T | PagingRequest<T>,
+  asyncThunk: AsyncThunk<PagingResult<R>, PagingRequest<T>, U>
+): UsePagingResult<[T]> => {
+  const initPaging: BasePagingRequest = {
+    pageNum: initParams.pageNum || 1,
+    pageSize: initParams.pageSize || 10
+  }
+  const initSurplusParams: T = { ...initParams }
+
+  delete initSurplusParams.pageNum
+  delete initSurplusParams.pageSize
+
+  const dispatch = useDispatch()
+  const getData = useCallback((paging: BasePagingRequest, params: T) => {
+    const action: any = asyncThunk({ ...paging, ...params })
+    return dispatch(action).unwrap()
+  }, [asyncThunk, dispatch])
+
+  return (usePaging as any)(getData, initPaging, initSurplusParams)
+}

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


+ 1 - 1
src/layout/top/index.tsx

@@ -29,7 +29,7 @@ export const Top = () => {
         <Sider className={style.sider}>
           { renderMenu }
         </Sider>
-        <Content>
+        <Content className={style.content}>
           <Outlet />
         </Content>
       </Layout>

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

@@ -9,4 +9,9 @@
   justify-content: space-between;
   align-items: center;
   padding: 0 20px;
+}
+
+.content {
+  overflow-y: auto;
+  flex: 1;
 }

+ 10 - 1
src/router/config.ts

@@ -1,7 +1,7 @@
 import { RoutePath } from 'constant'
 import { useLocation } from 'react-router-dom'
 import { verifiRoutePath } from 'utils'
-import { UpSquareOutlined } from '@ant-design/icons'
+import { UpSquareOutlined, ContainerOutlined } from '@ant-design/icons'
 
 import type { ComponentType, FunctionComponent } from 'react'
 
@@ -34,6 +34,14 @@ export const routesConfig: RoutesConfig = [
           label: '场景管理',
           icon: UpSquareOutlined
         }
+      },
+      {
+        path: RoutePath.example,
+        element: () => import('views/example'),
+        meta: {
+          label: '案件管理',
+          icon: ContainerOutlined
+        }
       }
     ]
   },
@@ -65,4 +73,5 @@ export const useRoute = () => {
 }
 
 export { RoutePath }
+export { ViewHome } from 'constant/route'
 export { fillRoutePath } from 'utils'

+ 48 - 5
src/router/index.tsx

@@ -1,10 +1,18 @@
-import { HashRouter as Router, useRoutes } from 'react-router-dom'
-import { lazy } from 'react'
+import { HashRouter as Router, useNavigate, useRoutes } from 'react-router-dom'
+import { lazy, useEffect } from 'react'
 import { AsyncComponent } from 'components'
-import { routesConfig } from './config'
+import { routesConfig, useRoute, RoutePath, ViewHome } from './config'
+import { 
+  addReqErrorHandler, 
+  addResErrorHandler, 
+  delReqErrorHandler,
+  delResErrorHandler,
+  ResCode, 
+} from 'api'
 
 import type { RouteObject } from 'react-router-dom'
 import type { RoutesConfig } from './config'
+import type { ResData } from 'api'
 
 export const getReactRouteConfig = (config: RoutesConfig): RouteObject[] => {
   return config.map(route => ({
@@ -16,9 +24,43 @@ export const getReactRouteConfig = (config: RoutesConfig): RouteObject[] => {
 
 export const GetRoutes = () => useRoutes(getReactRouteConfig(routesConfig))
 
+export const DefendRouter = ({ children }: { children: any }) => {
+  const route = useRoute()
+  const navigate = useNavigate()
+
+  // 默认去到首页
+  useEffect(() => {
+    if (!route) {
+      navigate(ViewHome)
+    } else if (route.path === RoutePath.home) {
+      navigate(ViewHome)
+    }
+  }, [route, navigate])
+
+  // token失效跳回login页
+  useEffect(() => {
+    const unTokenHandler = () => navigate(RoutePath.login, { replace: true })
+    const resErrorHandler = <T extends unknown>(_: any, data?: ResData<T>) => {
+      data?.code === ResCode.TOKEN_INVALID && unTokenHandler()
+    }
+
+    addReqErrorHandler(unTokenHandler)
+    addResErrorHandler(resErrorHandler)
+
+    return () => { 
+      delReqErrorHandler(unTokenHandler)
+      delResErrorHandler(resErrorHandler)
+    }
+  }, [navigate])
+
+  return children
+}
+
 export const AppRouter = () => (
   <Router>
-    <GetRoutes />
+    <DefendRouter>
+      <GetRoutes />
+    </DefendRouter>
   </Router>
 )
 
@@ -27,4 +69,5 @@ export {
   useNavigate,
   useHref,
   Outlet
-} from 'react-router-dom'
+} from 'react-router-dom'
+export { Router }

+ 1 - 1
src/setupProxy.js

@@ -4,7 +4,7 @@ const { createProxyMiddleware } = require('http-proxy-middleware')
 module.exports = function (app) {
   app.use(
     createProxyMiddleware('/api', {
-      target: 'http://120.25.146.52:3090/mock/193/',
+      target: 'http://192.168.0.38:8808',
       changeOrigin: true,
       pathRewrite: {
         '^/api': ''

+ 55 - 8
src/store/scene.ts

@@ -1,11 +1,23 @@
 import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
-import { getSceneByType } from 'api'
 import { initialThukState, createArrayThunkSelects, thunkStatusAutoSet } from './help'
+import { 
+  getSceneByType, 
+  getToken, 
+  uploadModelScene as uploadModelSceneApi,
+  deleteModelScene as deleteModelSceneApi
+} from 'api'
+import { 
+  ModelSceneStatus, 
+  QuoteSceneStatus, 
+  SceneType, 
+  SceneTypeDomain, 
+  SceneTypePaths 
+} from 'constant'
 
 import type { ThunkState } from './help'
-import type { Scenes } from 'api'
+import type { Scenes, Scene } from 'api'
 import type { StoreState } from './'
-import { SceneType } from 'constant'
+
 
 export type { Scenes, Scene } from 'api'
 export type SceneState = ThunkState<{value: Scenes}>
@@ -19,13 +31,18 @@ const sceneSlice = createSlice({
   name: 'scene',
   initialState,
   reducers: {
-
   },
   extraReducers(builder) {
     thunkStatusAutoSet(
       builder, 
       fetchScenes,
-      (state, scenes) => state.value = scenes
+      (state, data) => {
+        const types = new Set(data.list.map(scene => scene.type))
+        state.value = [
+          ...state.value.filter(scene => !types.has(scene.type)),
+          ...data.list
+        ]
+      }
     )
   }
 })
@@ -41,6 +58,36 @@ export const {
   'id'
 )
 
-export const fetchScenes = createAsyncThunk('fetch/scenes', async (type: SceneType) => {
-  return await getSceneByType(type)
-})
+export const fetchScenes = createAsyncThunk('fetch/scenes', getSceneByType)
+export const uploadModelScene = createAsyncThunk('upload/modelScene', uploadModelSceneApi)
+export const deleteModelScene = createAsyncThunk('delete/modelScene', deleteModelSceneApi)
+
+
+export enum SceneLinkFlag { query, edit }
+export const getSceneLink = (scene: Scene, flag: SceneLinkFlag): URL => {
+  const pathname = SceneTypePaths[scene.type][flag]
+  const domain = SceneTypeDomain[scene.type]
+  const url = new URL(pathname || '', domain)
+
+  if (scene.type !== SceneType.SWMX) {
+    url.searchParams.append('m', scene.num)
+  }
+
+  if (flag === SceneLinkFlag.edit) {
+    const token = getToken()
+    token && url.searchParams.append('token', token)
+  }
+  
+  return url
+}
+
+export const openSceneQueryPage = (scene: Scene) => window.open(getSceneLink(scene, SceneLinkFlag.query))
+export const openSceneEditPage = (scene: Scene) => window.open(getSceneLink(scene, SceneLinkFlag.edit))
+
+export const sceneIsSuccess = (scene: Scene) => 
+  (scene.type !== SceneType.SWMX && scene.status === QuoteSceneStatus.SUCCESS)
+    || (scene.type === SceneType.SWMX && scene.status === ModelSceneStatus.SUCCESS)
+
+export const sceneIsError = (scene: Scene) => 
+  (scene.type !== SceneType.SWMX && scene.status === QuoteSceneStatus.ERR)
+    || (scene.type === SceneType.SWMX && scene.status === ModelSceneStatus.ERR)

+ 19 - 4
src/store/user.ts

@@ -8,16 +8,28 @@ import type { StoreState } from 'store'
 
 export type { User } 
 
-const initialState: ThunkState<{ value: User }> = {
-  ...initialThukState,
-  value: {
+const userStoreKey = 'user'
+const getInitUser = (): User => {
+  const userString = localStorage.getItem(userStoreKey)
+  if (userString) {
+    try {
+      return JSON.parse(userString)
+    } catch { }
+  }
+  return {
     id: '0',
     userName: '游客',
+    nickName: '游客',
     head: '',
     cameraCount: 0
   }
 }
 
+const initialState: ThunkState<{ value: User }> = {
+  ...initialThukState,
+  value: getInitUser()
+}
+
 const userSlice = createSlice({
   name: 'user',
   initialState,
@@ -26,7 +38,10 @@ const userSlice = createSlice({
     thunkStatusAutoSet(
       builder, 
       postLogin,
-      (state, user) => state.value = user
+      (state, user) => {
+        state.value = user
+        localStorage.setItem(userStoreKey, JSON.stringify(user))
+      }
     )
   },
 })

+ 103 - 0
src/utils/encode.ts

@@ -0,0 +1,103 @@
+import { Base64 } from 'js-base64'
+
+function randomWord(randomFlag: boolean, min: number, max?: number) {
+  let str = ''
+  let range = min
+  let arr = [
+      '0',
+      '1',
+      '2',
+      '3',
+      '4',
+      '5',
+      '6',
+      '7',
+      '8',
+      '9',
+      'a',
+      'b',
+      'c',
+      'd',
+      'e',
+      'f',
+      'g',
+      'h',
+      'i',
+      'j',
+      'k',
+      'l',
+      'm',
+      'n',
+      'o',
+      'p',
+      'q',
+      'r',
+      's',
+      't',
+      'u',
+      'v',
+      'w',
+      'x',
+      'y',
+      'z',
+      'A',
+      'B',
+      'C',
+      'D',
+      'E',
+      'F',
+      'G',
+      'H',
+      'I',
+      'J',
+      'K',
+      'L',
+      'M',
+      'N',
+      'O',
+      'P',
+      'Q',
+      'R',
+      'S',
+      'T',
+      'U',
+      'V',
+      'W',
+      'X',
+      'Y',
+      'Z',
+  ]
+  // 随机产生
+  if (randomFlag && max) {
+      range = Math.round(Math.random() * (max - min)) + min
+  }
+  for (var i = 0; i < range; i++) {
+      let pos = Math.round(Math.random() * (arr.length - 1))
+      str += arr[pos]
+  }
+  return str
+}
+
+
+/**
+ * 密码加密
+ * @param {String} pwd
+ */
+ export function encodePwd(str: string, strv = '') {
+  str = Base64.encode(str)
+  const NUM = 2
+  const front = randomWord(false, 8)
+  const middle = randomWord(false, 8)
+  const end = randomWord(false, 8)
+
+  let str1 = str.substring(0, NUM)
+  let str2 = str.substring(NUM)
+
+  if (strv) {
+      let strv1 = strv.substring(0, NUM)
+      let strv2 = strv.substring(NUM)
+      return [front + str2 + middle + str1 + end, front + strv2 + middle + strv1 + end]
+  }
+
+  return front + str2 + middle + str1 + end
+}

+ 4 - 1
src/utils/index.ts

@@ -1,3 +1,6 @@
 export * from './array'
 export * from './base'
-export * from './route'
+export * from './route'
+export * from './encode'
+export * from './setState'
+export * from './serve'

+ 16 - 0
src/utils/serve.ts

@@ -0,0 +1,16 @@
+
+export const jsonToForm = (data: { [key in string]: any }) => {
+  const formData = new FormData()
+  for (const [key, val] of Object.entries(data)) {
+    formData.append(key, val)
+  }
+  return formData
+}
+
+export type UploadProgressCallback = (num: number) => void
+export const uploadProgressFactory = (callback: UploadProgressCallback) => (event: any) => {
+  if(event.lengthComputable){
+    const complete = (event.loaded / event.total) * 100 || 0
+    callback(complete || 0)
+  }
+}

+ 16 - 0
src/utils/setState.ts

@@ -0,0 +1,16 @@
+import { Dispatch, SetStateAction } from "react"
+
+export type QuickSetState<T> = (setStateRaw: Dispatch<SetStateAction<T>>) => 
+  (state: Partial<T> | ((state: T) => T)) => void
+
+export const quickSetState = <T>(
+  setStateRaw: Parameters<QuickSetState<T>>['0']
+): ReturnType<QuickSetState<T>> => {
+  return (state: Partial<T> | ((state: T) => T)) => {
+    const setState = typeof state === 'function' 
+      ? state 
+      : (basestate: T) => ({ ...basestate, ...state })
+
+    setStateRaw(setState)
+  }
+}

+ 5 - 0
src/views/example/index.tsx

@@ -0,0 +1,5 @@
+export const ExamplePage = () => {
+  return <div>123123</div>
+}
+
+export default ExamplePage

+ 20 - 7
src/views/login/index.tsx

@@ -1,9 +1,13 @@
 import { title } from 'constant'
-import { Input, Form, Button } from 'antd'
+import { Input, Form, Button, Spin } from 'antd'
 import { UserOutlined, LockOutlined } from '@ant-design/icons'
 import { Background } from 'components'
 import { useDispatch, postLogin } from 'store'
 import style from './style.module.scss'
+import { RoutePath, useNavigate } from 'router'
+import { useLoading } from 'hook'
+
+import type { LoginParams } from 'api'
 
 const loginInputs = [
   {
@@ -23,21 +27,30 @@ const loginInputs = [
 
 export const Login = () => {
   const dispatch = useDispatch()
+  const navigate = useNavigate()
+  const [loading, setPromise] = useLoading()
+
   const renderOptions = loginInputs.map(({node, ...props}, i) => (
     <Form.Item {...props} key={i} children={node} />
   ))
+  const loginHandler = async (data: LoginParams) => {
+    await setPromise(dispatch(postLogin(data)).unwrap())
+    navigate(RoutePath.home, { replace: true })
+  }
 
   return (
     <Background>
       <div className={style['login-layout']}>
         <div className={style['login']}>
           <h1>{title}</h1>
-          <Form 
-            name="login"
-            onFinish={data => dispatch(postLogin(data))}
-            autoComplete="off"
-            children={renderOptions}
-          />
+          <Spin spinning={loading} tip="登录中">
+            <Form 
+              name="login"
+              onFinish={loginHandler}
+              autoComplete="off"
+              children={renderOptions}
+            />
+          </Spin>
         </div>
       </div>
     </Background>

+ 129 - 0
src/views/scene/cloumns.tsx

@@ -0,0 +1,129 @@
+import { Button } from 'antd'
+import { 
+  SceneType, 
+  QuoteSceneStatusDesc, 
+  ModelSceneStatusDesc, 
+  ModelSceneStatus 
+} from 'constant';
+import { 
+  openSceneEditPage, 
+  openSceneQueryPage, 
+  sceneIsSuccess,
+  deleteModelScene,
+  useDispatch
+} from 'store'
+
+import type { ModelScene, QuoteScene, Scene } from 'api'
+import type { ColumnsType } from 'antd/es/table';
+
+type Column<T = Scene> = ColumnsType<T>[number]
+
+const titleColumn: Column = {
+  title: '名称',
+  dataIndex: 'title',
+  key: 'title',
+}
+
+const sncodeColumn: Column = {
+  title: 'SN码',
+  dataIndex: 'snCode',
+  key: 'snCode',
+}
+const timeColumn: Column = {
+  title: '拍摄时间',
+  dataIndex: 'createTime',
+  key: 'createTime',
+}
+const quoteStatusColumn: Column<QuoteScene> = {
+  title: '状态',
+  dataIndex: 'status',
+  key: 'status',
+  render: (_, record) => QuoteSceneStatusDesc[record.status]
+}
+
+
+const rawTypeColumn: Column<ModelScene> = {
+  title: '原始数据格式',
+  dataIndex: 'rawType',
+  key: 'rawType',
+}
+
+const modelStatusColumn: Column<ModelScene> = {
+  title: '状态',
+  dataIndex: 'status',
+  key: 'status',
+  render: (_, record) => {
+    let desc = ModelSceneStatusDesc[record.status]
+
+    if (record.status === ModelSceneStatus.RUN && record.progress) {
+      desc += ` ${record.progress}% `
+    } else if (record.status === ModelSceneStatus.SUCCESS) {
+      desc = record.time
+    }
+
+    const color = {
+      ghost: record.status === ModelSceneStatus.ERR,
+      primary: record.status === ModelSceneStatus.RUN
+    }
+
+    return <Button type="text" {...color} children={desc} />
+  }
+}
+
+export const useTypeColumns = (type: SceneType, refresh?: () => void): Column<Scene>[] => {
+  const dispatch = useDispatch()
+  const actionColumn: Column = {
+    title: '操作',
+    key: 'action',
+    render: (_, record) => {
+      type Action = { text: any, action: () => any, bind?: { [key in string]: any } }
+      const actions: Action[] = []
+        
+      if (sceneIsSuccess(record)) {
+        actions.push(
+          { text: '查看', action: openSceneQueryPage.bind(null, record) },
+          { text: '编辑', action: openSceneEditPage.bind(null, record) }
+        )
+      }
+        
+      if (record.type === SceneType.SWKK) {
+        actions.push({ text: '仿真', action: () => {} })
+      } else if (record.type === SceneType.SWMX && record.status !== ModelSceneStatus.RUN) {
+        actions.push({ 
+          text: '删除', 
+          action: () => dispatch(deleteModelScene(record.id)), 
+          bind: { danger: true } 
+        })
+      }
+  
+      return actions.map(({text, action, bind}) => 
+        <Button 
+          type="link" 
+          onClick={action} 
+          {...(bind || {})}
+          key={text}
+          children={text} />
+      )
+    }
+  }
+
+  switch(type) {
+    case SceneType.SWKJ:
+    case SceneType.SWSS:
+    case SceneType.SWKK:
+      return [
+        titleColumn,
+        sncodeColumn,
+        timeColumn,
+        quoteStatusColumn,
+        actionColumn
+      ] as Column<Scene>[]
+    case SceneType.SWMX:
+      return [
+        titleColumn,
+        rawTypeColumn,
+        modelStatusColumn,
+        actionColumn
+      ] as Column<Scene>[]
+  }
+}

+ 2 - 1
src/views/scene/index.tsx

@@ -18,7 +18,8 @@ const ScenePage = () => {
       items={tabItems} 
       active={type} 
       onChange={type => setType(Number(type))} 
-      content={ <SceneList type={type} /> } />
+      renderContent={type => <SceneList type={type} />}
+    />
   )
 }
 

+ 66 - 15
src/views/scene/list.tsx

@@ -1,31 +1,82 @@
-import { SceneType } from "constant"
-import { Table } from 'antd'
-import { getTypeColumns } from './table-cloumns'
 import style from './style.module.scss'
-import { useEffect } from "react"
+import { SceneType } from "constant"
+import { memo } from 'react'
+import { Table, Input, Button, Upload, message } from 'antd'
+import { useTypeColumns } from './cloumns'
+import { useThunkPaging } from 'hook'
 import { 
-  useDispatch,
   useSelector, 
+  useDispatch,
+  filterScenesSelector,
   fetchScenes,
-  scenesSelector
+  uploadModelScene
 } from 'store'
 
+import { GetSceneByTypeParams, PagingRequest } from 'api'
+import type { UploadProps } from 'antd'
 
-export type SceneListProps = {type: SceneType}
+const { Search } = Input
 
-export const SceneList = ({ type }: SceneListProps) => {
+type ListHeaderProps = SceneListProps & {
+  onDataChange?: () => void,
+  onSearch: (params: (GetSceneByTypeParams extends PagingRequest<infer T> ? Omit<T, 'type'> : never)) => void
+}
+const ListHeader = memo(({ type, onSearch, onDataChange }: ListHeaderProps) => {
   const dispatch = useDispatch()
-  const scenes = useSelector(scenesSelector)
+  const onUpload: UploadProps['beforeUpload'] = file => {
+    const isZip = ['application/zip', 'application/rar'].includes(file.type)
+    if (!isZip) {
+      message.error('只能上传zip或rar文件')
+    }
+    dispatch(uploadModelScene({ file }))
+      .unwrap()
+      .finally(onDataChange)
+    return Upload.LIST_IGNORE
+  }
 
-  useEffect(() => {
-    dispatch(fetchScenes(type))
-  }, [dispatch, type])
+  const renderUpload = type === SceneType.SWMX && (
+    <Upload beforeUpload={onUpload} multiple={false} >
+      <Button type="primary" children="上传数据" />
+    </Upload>
+  )
 
   return (
-    <div className={style['table-body']}>
-      <Table columns={getTypeColumns(type)} dataSource={scenes} rowKey="title" />
+    <div className={style['table-header']}>
+      { renderUpload }
+      <Search
+        className={style['table-search']}
+        placeholder="输入名称搜索" 
+        onSearch={sceneName => onSearch({ sceneName }) }
+        style={{ width: 264 }} 
+      />
     </div>
   )
-}
+})
+
+export type SceneListProps = { type: SceneType }
+export const SceneList = memo(({ type }: SceneListProps) => {
+  const scenes = useSelector((state) => filterScenesSelector(state, { type }))
+  const states = useThunkPaging({ type, sceneName: '' }, fetchScenes)
+  const [[paging, setPaging], [, setParams], refresh] = states
+  const cloumns = useTypeColumns(type, refresh)
+
+  return (
+    <div className={style['table-layout']}>
+      <ListHeader type={type} onSearch={setParams} onDataChange={refresh} />
+
+      <Table
+        columns={cloumns} 
+        dataSource={scenes} 
+        rowKey="id"
+        pagination={{
+          showSizeChanger: false,
+          pageSize: paging.pageSize,
+          total: paging.total,
+          onChange: (page, pageSize) => setPaging({ pageNum: page, pageSize })
+        }}
+      />
+    </div>
+  )
+})
 
 export default SceneList

+ 11 - 1
src/views/scene/style.module.scss

@@ -1,3 +1,13 @@
-.table-body {
+.table-layout {
   padding: 0 24px;
+}
+
+.table-header {
+  padding: 24px 0;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.table-search {
+  margin-left: 20px;
 }

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

@@ -1,66 +0,0 @@
-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
-      ]
-  }
-}

+ 11 - 12
yarn.lock

@@ -4958,7 +4958,7 @@ http-proxy-agent@^4.0.1:
     agent-base "6"
     debug "4"
 
-http-proxy-middleware@^2.0.3:
+http-proxy-middleware@^2.0.3, http-proxy-middleware@^2.0.6:
   version "2.0.6"
   resolved "https://registry.npmmirror.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f"
   integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==
@@ -4969,17 +4969,6 @@ http-proxy-middleware@^2.0.3:
     is-plain-obj "^3.0.0"
     micromatch "^4.0.2"
 
-http-proxy-middleware@^2.0.6:
-  version "2.0.6"
-  resolved "http://192.168.0.47:4873/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f"
-  integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==
-  dependencies:
-    "@types/http-proxy" "^1.17.8"
-    http-proxy "^1.18.1"
-    is-glob "^4.0.1"
-    is-plain-obj "^3.0.0"
-    micromatch "^4.0.2"
-
 http-proxy@^1.18.1:
   version "1.18.1"
   resolved "https://registry.npmmirror.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
@@ -5868,6 +5857,11 @@ jest@^27.4.3:
     import-local "^3.0.2"
     jest-cli "^27.5.1"
 
+js-base64@^3.7.2:
+  version "3.7.2"
+  resolved "http://192.168.0.47:4873/js-base64/-/js-base64-3.7.2.tgz#816d11d81a8aff241603d19ce5761e13e41d7745"
+  integrity sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==
+
 "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
   version "4.0.0"
   resolved "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -6335,6 +6329,11 @@ minimist@^1.2.0, minimist@^1.2.6:
   resolved "https://registry.npmmirror.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
   integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
 
+mitt@^3.0.0:
+  version "3.0.0"
+  resolved "http://192.168.0.47:4873/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd"
+  integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==
+
 mkdirp@~0.5.1:
   version "0.5.6"
   resolved "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"