Jelajahi Sumber

fix: 添加场景管理

bill 3 tahun lalu
induk
melakukan
e9a1f8d15e

+ 2 - 1
package.json

@@ -17,6 +17,7 @@
     "axios": "^0.19.0",
     "cesium": "1.64.0",
     "copy-webpack-plugin": "^5.0.5",
+    "date-fns": "^2.28.0",
     "history": "^4.10.1",
     "ol": "4.3.3",
     "react": "^16.12.0",
@@ -28,7 +29,7 @@
     "react-router-dom": "^5.1.2",
     "react-scripts": "3.2.0",
     "redux": "^4.2.0",
-    "typescript": "4.4.4"
+    "typescript": "^4.7.4"
   },
   "scripts": {
     "start": "react-app-rewired start",

+ 4 - 0
public/style.css

@@ -74,4 +74,8 @@ input {
 .cesium-viewer-fullscreenContainer,
 .cesium-viewer-bottom {
   display: none !important;
+}
+
+.ant-empty-normal {
+  color: rgba(255, 255, 255, 0.25);;
 }

+ 2 - 1
src/http.ts

@@ -22,10 +22,11 @@ axios.interceptors.request.use(request => {
 })
 
 axios.interceptors.response.use(response => {
-  if (response.data.code === 401) {
+  if (response.data.code === 401 || response.data.status === 5001) {
     sessionStorage.removeItem('token')
     token = null;
     replaceLogin()
+    throw new Error('当前用户未登录')
   }
 
   return response

+ 19 - 19
src/index.tsx

@@ -4,19 +4,17 @@ import "antd/dist/antd.css";
 import "./style.css";
 import * as serviceWorker from './serviceWorker';
 import {Route, Router} from 'react-router'
-import config, {history} from './router.config'
+import config, {history, RouteItem} from './router.config'
 import Header from './layout/Header'
 import Slide from './layout/Slide';
 import Combination from './layout/Combination'
-import { RouteComponentProps, Switch, Redirect } from 'react-router'
-import Login from './page/Login'
+import { RouteComponentProps, Switch } from 'react-router'
 import {Provider} from 'react-redux'
 import store from './store'
 
 function App() {
-  let [route] = useState<any>()
-  
-  let Items = config.map(item => (
+  const [route] = useState<any>()
+  const renderRoutes = (config: RouteItem[]) => config.map(item => (
     <Route
       key={item.path}
       path={item.path}
@@ -30,25 +28,27 @@ function App() {
       )}
     />
   ))
+  const indetRoutes = renderRoutes(config.filter(route => !route.joinTab))
+  const jointRoutes = renderRoutes(config.filter(route => route.joinTab))
 
+  
   return (
-    <Router history={history}>
-      <Switch>
-        <Provider store={store}>
-          <Route path="/login" component={Login} />
-          <div className="app">
-            <Header className='header' />
+    <Provider store={store}>
+      <div className="app">
+        <Header className='header' />
+        <Router history={history}>
+          <Switch>
+            {indetRoutes}
             <div className='section'>
-              <Route path="/" component={() => <Slide {...route} className='slide' />} />
+              <Route path="/:sid" component={() => <Slide {...route} className='slide' />} />
               <Switch>
-                {Items}
-                {/* <Redirect to="/gis" /> */}
+                {jointRoutes}
               </Switch>
             </div>
-          </div>
-        </Provider>
-      </Switch>
-    </Router>
+          </Switch>
+        </Router>
+      </div>
+    </Provider>
   )
 }
 

+ 9 - 3
src/layout/Slide.tsx

@@ -1,10 +1,14 @@
 import React from 'react'
 import config from '../router.config'
-import { Link } from 'react-router-dom'
+import { Link, useHistory, useRouteMatch } from 'react-router-dom'
 import style from './style.module.css'
 import { RouteProps } from '../@types'
+import { fullRouteParams } from '../util'
 
 function Slide(props: RouteProps) {
+  const match = useRouteMatch()
+  const sid = (match?.params as any).sid
+  const history = useHistory()
   let router = config.filter(item => item.navigation)
   let query = (item: any, currItem: any) : boolean => {
     if (!currItem) return false
@@ -17,12 +21,14 @@ function Slide(props: RouteProps) {
   }
   const currItem = props.match && config.find(item => item.path === props.match.path)
 
-          // <Redirect from="/" to="/gis" />
+  if (!sid) {
+    history.push('/scene')
+  }
 
   return (
     <div className={style.slidelayer + ' ' + props.className}>
       {router.map(item => (
-        <Link to={item.path} key={item.path} className={query(item, currItem) ? style.active : ''} >
+        <Link to={fullRouteParams(item.path, {sid})} key={item.path} className={query(item, currItem) ? style.active : ''} >
           <img src={item.icon} alt={item.icon} />
           {item.title}
         </Link>

+ 9 - 1
src/layout/SubHead.tsx

@@ -1,14 +1,22 @@
+import { Button } from 'antd';
 import React from 'react'
+import { Link } from 'react-router-dom';
 import { RouteProps } from '../@types';
 import config from '../router.config'
 import style from './style.module.css'
 
 function SubHead(props: RouteProps) {
-  let item = config.find(item => props.match.path === item.path)
+  const item = config.find(item => props.match.path === item.path)
+  const renderHomeBtn = item.joinTab && (
+    <Button type="primary" className={style.manage}>
+      <Link to="/scene">场景管理</Link>
+    </Button>
+  )
   
   return item ? (
     <h2 className={style.subhead}>
       {item.title}
+      { renderHomeBtn }
     </h2>
   ): null
 }

+ 4 - 0
src/layout/style.module.css

@@ -27,4 +27,8 @@
 .subhead {
   font-size: 18px;
   margin-bottom: 20px;
+}
+
+.manage {
+  float: right;
 }

+ 7 - 3
src/page/List/GeoList.tsx

@@ -1,17 +1,21 @@
 import React, { useState } from 'react'
 import GrentReducer from './grent'
 import Upload from '../../components/Upload'
-import { Link } from 'react-router-dom'
+import { Link, useRouteMatch } from 'react-router-dom'
 import styles from './index.module.css'
 import { sectionStepAction } from './ListState'
 import Step from '../../components/Upload/Step'
 import Coor from './Coor'
 import path from 'path'
+import { fullRouteParams } from '../../util'
 
 
 const intervals: Array<any> = []
 
 export default function GeoList({ className }: any) {
+  const match = useRouteMatch()
+  const sid = (match?.params as any).sid
+
   intervals.forEach(interval => clearInterval(interval))
   const { referData, Element, models, referItem, modelDispatch } = GrentReducer({
     delUrl: '/vector/delete/',
@@ -25,7 +29,7 @@ export default function GeoList({ className }: any) {
       if (model.status === 6) {
         return <Step step={model.sectStep ? model.sectStep : 0} />
       } else if (model.status === 8 || model.status === 11 || model.status === 12) {
-        return <Link to={"/style/" + model.id} style={{ color: '#3e7cd3' }}>编辑样式</Link>
+        return <Link to={fullRouteParams('/:sid/style/:id', {sid, id: model.id})} style={{ color: '#3e7cd3' }}>编辑样式</Link>
       }
     },
     region: true
@@ -44,7 +48,7 @@ export default function GeoList({ className }: any) {
   let coord = coorstr.split(',').length > 1 ? coorstr.split(',') : []
 
   async function getStep(model: Model) {
-    let data = await sectionStepAction(modelDispatch, `/vector/progress/${model.id}/`, model)
+    let data = await sectionStepAction(modelDispatch, sid, `/vector/progress/${model.id}/`, model)
     return data.data && data.data.progress
   };
 

+ 9 - 5
src/page/List/GrentOper.tsx

@@ -3,6 +3,7 @@ import { zipItemAction, sectionItemAction, judgeItemAction, transferItemAction }
 import Item from '../../components/item'
 import Dialog from '../../components/Dialog'
 import styles from './index.module.css'
+import { useRouteMatch } from 'react-router-dom'
 
 const JUDGEING = 1, JUGESUCCESS = 2, JUGEERR = 3
 const SECTIONING = 4, SECTIONSUCCESS = 5, SECTIONEERR = 6
@@ -12,6 +13,9 @@ const TRANSFERING = 13, TRANSFERSUCCESS = 14, TRANSFEREERR = 15
 
 
 export default function Grent({ setItemStaus, modelDispatch, referData, delHandle, api, region }: any) {
+  const match = useRouteMatch()
+  const sid = (match?.params as any).sid
+  
   let [identity, setIdentity] = ['', (dom: HTMLSelectElement) => { identity = dom.value }]
   let [text, setText] = ['', (dom: HTMLInputElement) => { text = dom.value }]
   let [min, setMin] = ['9', (dom: HTMLInputElement) => {
@@ -35,7 +39,7 @@ export default function Grent({ setItemStaus, modelDispatch, referData, delHandl
 
   const judge = async (model: Model) => {
     model = setItemStaus(model, JUDGEING)
-    let data = await judgeItemAction(modelDispatch, api.judge + model.id + '/', model)
+    let data = await judgeItemAction(modelDispatch, sid, api.judge + model.id + '/', model)
 
     if (data.status !== 200) {
       alert(data.message)
@@ -47,7 +51,7 @@ export default function Grent({ setItemStaus, modelDispatch, referData, delHandl
   }
   const zipHandle = async (model: Model) => {
     model = setItemStaus(model, ZIPING)
-    let data = await zipItemAction(modelDispatch, api.zip + model.id + '/', model)
+    let data = await zipItemAction(modelDispatch, sid, api.zip + model.id + '/', model)
     if (data.status !== 200) {
       alert(data.message)
       setItemStaus(model, ZIPEERR)
@@ -60,7 +64,7 @@ export default function Grent({ setItemStaus, modelDispatch, referData, delHandl
   const section = async (model: Model) => {
     let param = region ? (min + '/' + max + '/') : ''
     model = setItemStaus(model, SECTIONING)
-    sectionItemAction(modelDispatch, api.section + model.id + '/' + param, model)
+    sectionItemAction(modelDispatch, sid, api.section + model.id + '/' + param, model)
       .then(data => {
         if (data.status !== 200) {
           model = setItemStaus(model, SECTIONEERR)
@@ -74,7 +78,7 @@ export default function Grent({ setItemStaus, modelDispatch, referData, delHandl
 
   const transform = async (model: Model) => {
     model = setItemStaus(model, TRANING)
-    sectionItemAction(modelDispatch, api.transform + model.id + '/', model)
+    sectionItemAction(modelDispatch, sid, api.transform + model.id + '/', model)
       .then(data => {
         if (data.status !== 200) {
           model = setItemStaus(model, TRANEERR)
@@ -88,7 +92,7 @@ export default function Grent({ setItemStaus, modelDispatch, referData, delHandl
 
   const transfer = async (model: Model) => {
     model = setItemStaus(model, TRANSFERING)
-    transferItemAction(modelDispatch, api.transfer + model.id + '/', model, { text: text, role: identity })
+    transferItemAction(modelDispatch, sid, api.transfer + model.id + '/', model, { text: text, role: identity })
       .then(data => {
         if (data.status !== 200) {
           model = setItemStaus(model, TRANSFEREERR)

+ 14 - 7
src/page/List/ListState/action.ts

@@ -51,7 +51,8 @@ function getStateLocal(item: any) {
 }
 
 
-export const getListAction = async (dispatch: Function, url: string, current: number) => {
+export const getListAction = async (dispatch: Function, sid: string, url: string, current: number) => {
+  console.log(sid)
   let res = await http.post(url, { "pageNum": current - 1, "pageSize": 10})
   let list = res.data?.data?.content || []
 
@@ -81,7 +82,8 @@ export const getListAction = async (dispatch: Function, url: string, current: nu
   })
 }
 
-export const delItemAction = async (dispatch: Function, url: string, item: Model) => {
+export const delItemAction = async (dispatch: Function, sid: string, url: string, item: Model) => {
+  console.log(sid)
   await http.get(url)
   dispatch({
     type: DEL_ITEM,
@@ -89,22 +91,26 @@ export const delItemAction = async (dispatch: Function, url: string, item: Model
   })
 }
 
-export const zipItemAction = async (dispatch: Function, url: string, item: Model) => {
+export const zipItemAction = async (dispatch: Function, sid: string, url: string, item: Model) => {
+  console.log(sid)
   let res = await http.get(url)
   return res.data
 }
 
-export const judgeItemAction = async (dispatch: Function, url: string, item: Model) => {
+export const judgeItemAction = async (dispatch: Function, sid: string, url: string, item: Model) => {
+  console.log(sid)
   let res = await http.get(url)
   return res.data
 }
 
-export const sectionItemAction = async (dispatch: Function, url: string, item: Model) => {
+export const sectionItemAction = async (dispatch: Function, sid: string, url: string, item: Model) => {
+  console.log(sid)
   let res = await http.get(url)
   return res.data
 }
 
-export const sectionStepAction = async (dispatch: Function, url: string, item: Model) => {
+export const sectionStepAction = async (dispatch: Function, sid: string, url: string, item: Model) => {
+  console.log(sid)
   let res = await http.get(url)
   if (res.data.data && res.data.data.progress) {
     sessionStorage.setItem(res.data.data.id, res.data.data.progress)
@@ -112,7 +118,8 @@ export const sectionStepAction = async (dispatch: Function, url: string, item: M
   return res.data
 }
 
-export const transferItemAction = async (dispatch: Function, url: string, item: Model, body: any) => {
+export const transferItemAction = async (dispatch: Function, sid: string, url: string, item: Model, body: any) => {
+  console.log(sid)
   let res = await http.post(url, body)
   return res.data
 }

+ 5 - 1
src/page/List/ResterList.tsx

@@ -5,10 +5,14 @@ import styles from './index.module.css'
 import { sectionStepAction } from './ListState'
 import Step from '../../components/Upload/Step'
 import Coor from './Coor'
+import { useRouteMatch } from 'react-router-dom'
 
 const intervals: Array<any> = []
 
 export default function ModelList({ className }: any) {
+  const match = useRouteMatch()
+  const sid = (match?.params as any).sid
+  
   intervals.forEach(interval => clearInterval(interval))
   const { referData, Element, modelDispatch, models, referItem } = GrentReducer({
     judgeUrl: '/raster/command/judge/coord/',
@@ -26,7 +30,7 @@ export default function ModelList({ className }: any) {
   let [coorstr, setCoor] = useState('')
 
   async function getStep(model: Model) {
-    let data = await sectionStepAction(modelDispatch, `/raster/progress/${model.id}/`, model)
+    let data = await sectionStepAction(modelDispatch, sid, `/raster/progress/${model.id}/`, model)
     return data.data.progress
   };
 

+ 7 - 2
src/page/List/grent.tsx

@@ -12,6 +12,7 @@ import {
   delItemAction,
   updateItemAction
 } from './ListState'
+import { useRouteMatch } from 'react-router-dom'
 
 interface ItemsProps {
   list: Array<any>,
@@ -57,15 +58,19 @@ interface GrentApi {
 
 const useEffectRaw = useEffect
 export default function GrentReducer1({ getUrl, delUrl, zipUrl, sectionUrl, transformUrl, transferUrl, judgeUrl, ItemFn, region }: GrentApi) {
+  const match = useRouteMatch()
+  const sid = (match?.params as any).sid
+
+  console.log('===>', sid)
   let [showDialog1, setShowDialog1] = useState(false)
   let [showDialog2, setShowDialog2] = useState(false)
   let [referCount, setReferCount] = useState(0)
   let [modelState, modelDispatch] = useReducer(reducer, initialState())
   const models = getList(modelState)
-  const updateAction = useCallback((current: number) => getListAction(modelDispatch, getUrl, current), [getUrl])
+  const updateAction = useCallback((current: number) => getListAction(modelDispatch, sid, getUrl, current), [getUrl, sid])
   const referData = () => setReferCount(++referCount)
   const delHandle = async (model: Model) => {
-    await delItemAction(modelDispatch, delUrl + model.id + '/', model)
+    await delItemAction(modelDispatch, sid, delUrl + model.id + '/', model)
     setReferCount(++referCount)
   }
   const referItem = (model: Model): Model => updateItemAction(modelDispatch, model)

+ 1 - 1
src/page/Login/index.tsx

@@ -14,7 +14,7 @@ function Login() {
     if (status === 200) {
       sessionStorage.setItem('userName', data.name)
       setToken(data.token)
-      history.replace('/gis')
+      history.replace('/scene')
     } else {
       alert(message)
     }

+ 6 - 3
src/page/StyleEdit/index.tsx

@@ -1,5 +1,5 @@
 import React, { useReducer, useEffect, useState, Fragment } from 'react'
-import { RouteComponentProps } from 'react-router'
+import { RouteComponentProps, useRouteMatch } from 'react-router'
 import { reducer, initialState, getLayersAction, updateLayerAction, saveLayersAction, Item } from './reducer'
 import VectorShow from '../../components/VectorShow'
 import Color from '../../components/Color'
@@ -12,6 +12,9 @@ type Attr = 'lineColor' | 'lineWidth' | 'fillColor' | 'show'
 type EvAttr = 'value' | 'checked'
 
 function StyleEdit(props: RouteComponentProps) {
+  const match = useRouteMatch()
+  const sid = (match?.params as any).sid
+
   let [state, dispatch] = useReducer(reducer, initialState)
   let layers = Object.keys(state)
   let [layer, setLayer] = useState('')
@@ -19,14 +22,14 @@ function StyleEdit(props: RouteComponentProps) {
   let style = (layer && state[layer]) as Item
   const hots = useSelector<StoreState, Hots>(state => state.hots)
 
-  useEffect(() => { getLayersAction(dispatch, params.id) }, [params.id])
+  useEffect(() => { getLayersAction(dispatch, sid, params.id) }, [params.id, sid])
   useEffect(() => { layer || setLayer(layers[0]) }, [layer, layers])
 
   const changeHandle = (attr: Attr, evAttr: EvAttr = 'value', init = false) => (ev: any) => {
     updateLayerAction(dispatch, layer, { ...style, [attr]: init ? ev : ev.target[evAttr] })
   }
   const saveHandle = async () => {
-    if (await saveLayersAction(style, params.id)) {
+    if (await saveLayersAction(style, sid, params.id)) {
       alert('成功发布')
     } else {
       alert('发布失败')

+ 2 - 2
src/page/StyleEdit/reducer.ts

@@ -91,7 +91,7 @@ export const addLayerAction: ActionFun = (dispatch, layer, value = defaultLayerS
 export const updateLayerAction: ActionFun = (dispatch, layer, value) => dispatch({ type: UPDATELAYER, plyload: { layer, value } })
 export const delLayerAction: ActionFun = (dispatch, layer) => dispatch({ type: DELLAYER, plyload: { layer } })
 
-export const getLayersAction = async (dispatch: Dispatch, id: number) => {
+export const getLayersAction = async (dispatch: Dispatch, sid: string, id: number) => {
   let res = await http.get('/vector/style/get/' + id + '/')
   let data = res.data.data
   if (!data) return;
@@ -109,7 +109,7 @@ export const getLayersAction = async (dispatch: Dispatch, id: number) => {
   addLayerAction(dispatch, layer.name, style)
 }
 
-export const saveLayersAction = async (style: Item, id: number) => {
+export const saveLayersAction = async (style: Item, sid: string, id: number) => {
   try {
     let res = await http.post('/vector/style/save/', {
       outputFileId: id,

+ 27 - 0
src/page/scene/add-scene.tsx

@@ -0,0 +1,27 @@
+import React from 'react'
+import { PostFrom, usePostFromArgs } from './component'
+import { useDispatch } from '../../store'
+import { addScene } from '../../store/scene'
+import { Modal } from 'antd'
+
+
+export const AddScene = (props: {onClose: () => void}) => {
+  const dispatch = useDispatch()
+  const postFromArgs = usePostFromArgs(async post => {
+    const action = addScene(post)
+    await dispatch(action).unwrap()
+    props.onClose()
+  })
+
+  return (
+    <Modal 
+      title="添加场景" 
+      okText='添加'
+      cancelText='取消'
+      visible={true} 
+      onOk={postFromArgs.onSubmit} 
+      onCancel={props.onClose}>
+      <PostFrom {...postFromArgs} />
+    </Modal>
+  )
+}

+ 155 - 0
src/page/scene/component.tsx

@@ -0,0 +1,155 @@
+import type { Scene, EditableScene } from '../../store/scene'
+import { 
+  ChangeEvent, 
+  Dispatch, 
+  SetStateAction,
+  useCallback, 
+  useState,
+  useEffect
+} from 'react'
+import React from 'react'
+import { SceneType } from '../../store/scene'
+import { Button, Form, Input, Space, Upload, UploadFile, Select, message } from 'antd'
+import UploadOutlined from '@ant-design/icons/lib/icons/UploadOutlined'
+
+const { Option } = Select
+
+
+export const usePostFromArgs = (onSubmitRaw: (post: EditableScene) => void | any, post?: EditableScene) => {
+  const initCover = post?.cover || null
+  const initTitle = post?.title || ''
+  const initType = Number.isInteger(post?.type) ? post.type : SceneType.GIS
+  const initM = post?.m || ''
+
+  const titleState = useState(initTitle)
+  const coverState = useState(initCover)
+  const typeState = useState(initType)
+  const mState = useState(initM)
+
+  const checkInput = useCallback(() => {
+    const msg = 
+      !titleState[0] ? '请填写标题' :
+      !coverState[0] ? '请上传封面' :
+      (typeState[0] !== SceneType.GIS && !mState[0]) 
+        ? '非gis类别必须填写场景码' 
+        : null
+
+    msg && message.error(msg);
+    return !msg
+  }, [titleState, coverState, typeState, mState])
+
+  const onSubmit = useCallback(async () => {
+    if (checkInput()) {
+      const postRaw = {
+        type: typeState[0],
+        title: titleState[0],
+        cover: coverState[0],
+        m: mState[0]
+      }
+      try {
+        if (await onSubmitRaw(postRaw as any) !== false) {
+          titleState[1](initTitle)
+          coverState[1](initCover)
+          typeState[1](initType)
+          mState[1](initM)
+        }
+      } catch {
+
+      }
+    }
+  }, [titleState, coverState, onSubmitRaw, initTitle, initCover, typeState, mState, initType, initM, checkInput])
+
+  return {
+    title: titleState,
+    cover: coverState,
+    type: typeState,
+    m: mState,
+    onSubmit
+  }
+}
+
+type FromScene = {
+  [key in keyof EditableScene]: Array<any> & {
+    0: Scene[key], 
+    1: (Dispatch<SetStateAction<Scene[key]>>)
+  }
+}
+type SceneFromProps<T extends FromScene> = T & { onSubmit: () => void }
+
+export const PostFrom = <T extends FromScene>({onSubmit, ...article }: SceneFromProps<T>) => {
+  const [title, setTitle] = article.title
+  const [cover, setCover] = article.cover
+  const [type, setType] = article.type
+  const [m, setM] = article.m
+  const [fileList, setFileList] = useState<UploadFile[]>([])
+
+  useEffect(() => {
+    if (!cover) return;
+    setFileList([{
+      uid: '-1',
+      name: '封面图',
+      status: 'done',
+      url: cover,
+      type: 'image/png'
+    }])
+  }, [cover])
+
+  useEffect(() => {
+    if (type === SceneType.GIS) {
+      setM('')
+    }
+  }, [type, setM])
+
+  const onTitleChanged = (e: ChangeEvent<HTMLInputElement>) => setTitle(e.target.value)
+  const onMChanged = (e: ChangeEvent<HTMLInputElement>) => setM(e.target.value)
+  const handleChange = (info: {fileList: UploadFile[]}) => {
+    info.fileList[0].status === 'done' &&
+      setCover(info.fileList[0].response.url)
+    setFileList(info.fileList)
+  }
+
+  const typeOptions = [
+    { value: SceneType.GIS, label: 'gis' },
+    { value: SceneType.KANKAN, label: '四维看看' },
+    { value: SceneType.LASER, label: '激光场景' },
+  ]
+  const renderOptions = typeOptions.map(option => (
+    <Option value={option.value} key={option.value}>
+      {option.label}
+    </Option>
+  ))
+  const renderMInput = type !== SceneType.GIS && (
+      <Form.Item label="场景码">
+        <Input value={m} onChange={onMChanged} />
+      </Form.Item>
+    )
+  return (
+    <Form
+      labelCol={{ span: 8 }}
+      wrapperCol={{ span: 16 }}
+    >
+      <Form.Item label="场景名称">
+        <Input value={title} onChange={onTitleChanged} />
+      </Form.Item>
+      <Form.Item label="场景类别">
+        <Select defaultValue={ type } onChange={value => setType(value)}>
+          { renderOptions }
+        </Select>
+      </Form.Item>
+      { renderMInput }
+      <Form.Item label="封面图">
+        <Space direction="vertical" style={{ width: '100%' }} size="large">
+          <Upload
+            action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
+            listType="picture"
+            fileList={fileList}
+            onChange={handleChange}
+            maxCount={1}
+          >
+            <Button icon={<UploadOutlined translate={undefined} />}>上传</Button>
+          </Upload>
+        </Space>
+      </Form.Item>
+    </Form>
+  )
+}

+ 31 - 0
src/page/scene/edit-scene.tsx

@@ -0,0 +1,31 @@
+import React from 'react'
+import { PostFrom, usePostFromArgs } from './component'
+import { useDispatch } from '../../store'
+import { updateScene, Scene } from '../../store/scene'
+import { Modal } from 'antd'
+
+
+export const EditScene = ({onClose, ...scene}: {onClose: () => void} & Scene) => {
+  const dispatch = useDispatch()
+  const postFromArgs = usePostFromArgs(async nscene => {
+    const action = updateScene({
+      ...scene,
+      ...nscene
+    })
+    await dispatch(action).unwrap()
+    onClose()
+    return false
+  }, scene)
+
+  return (
+    <Modal 
+      title="更新场景" 
+      okText='修改'
+      cancelText='取消'
+      visible={true} 
+      onOk={postFromArgs.onSubmit} 
+      onCancel={onClose}>
+      <PostFrom {...postFromArgs} />
+    </Modal>
+  )
+}

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

@@ -0,0 +1,34 @@
+import { Row, Empty, Button } from 'antd'
+import React, { useState } from 'react'
+import { useSelector } from '../../store'
+import { scenesSelector } from '../../store/scene'
+import { AddScene } from './add-scene'
+import { SignScene } from './sign-scene'
+import style from './style.module.css'
+
+
+export const SceneList = () => {
+  const [adding, setAdding] = useState(false)
+  const scenes = useSelector(scenesSelector)
+  const renderScenes = scenes.map(scene => <SignScene {...scene} key={scene.id} />)
+  const renderLayer = scenes.length
+    ? <Row gutter={16}>{renderScenes}</Row>
+    : <Empty description="暂无数据" image={Empty.PRESENTED_IMAGE_SIMPLE}>
+       { <Button type="primary" onClick={() => setAdding(true)}>立即创建</Button> } 
+      </Empty>
+  const renderHeader = !scenes.length
+    ? null
+    : <div className={style.header}>
+        <Button type="primary" onClick={() => setAdding(true)}>创建场景</Button>
+      </div>
+
+  return (
+    <>
+      { renderHeader }
+      <div className="site-card-wrapper">
+        { adding && <AddScene onClose={() => setAdding(false)} /> }
+        { renderLayer }
+      </div>
+    </>
+  )
+}

+ 94 - 0
src/page/scene/sign-scene.tsx

@@ -0,0 +1,94 @@
+import React, { useState } from 'react'
+
+import { useDispatch } from '../../store'
+import { Scene, deleteScene, SceneType } from '../../store/scene'
+import { formatDistanceToNow, parseISO } from 'date-fns'
+import zh from 'date-fns/locale/zh-CN'
+
+import { EditScene } from './edit-scene'
+import { Card, Col, Avatar, Modal } from 'antd'
+import { EditOutlined, PaperClipOutlined, DeleteOutlined, ExclamationCircleOutlined, SettingOutlined } from '@ant-design/icons';
+import style from './style.module.css'
+import { useHistory } from 'react-router'
+
+const { Meta } = Card;
+const confirm = (msg: string) => {
+  return new Promise(resolve => {
+    Modal.confirm({
+      title: '系统提示',
+      icon: <ExclamationCircleOutlined translate={undefined} />,
+      content: msg,
+      okText: '确认',
+      cancelText: '取消',
+      onOk: () => resolve(true),
+      onCancel: () => resolve(false)
+    });
+  })
+};
+
+const useSceneLinks = (scene: Scene) => {
+  const history = useHistory()
+  const goto = (link: string) => () => window.location.href = link
+
+  switch (scene.type) {
+    case SceneType.GIS:
+      return {
+        gotoScene: goto(`http://map.4dage.com/3dmap?sid=${scene.id}`),
+        editScene: () => history.push(`/${scene.id}/grid`)
+      }
+    case SceneType.LASER:
+      const gotoLink = goto(`https://laser.4dkankan.com/index.html?m=${scene.m}`)
+      return {
+        gotoScene: gotoLink,
+        editScene: gotoLink
+      }
+    case SceneType.KANKAN:
+      return {
+        gotoScene: goto(`https://www.4dkankan.com/spg.html?m=${scene.m}`),
+        editScene: goto(`https://www.4dkankan.com/epg.html?m=${scene.m}`)
+      }
+  }
+}
+
+export const SignScene = (scene: Scene) => {
+  const [editIng, setEditIng] = useState(false)
+  const date = parseISO(scene.time)
+  const timeAgo = formatDistanceToNow(date, { locale: zh })
+  const dispatch = useDispatch()
+  const deleteHandler = async () => {
+    if (await confirm('确定要删除此场景?')) {
+      await dispatch(deleteScene(scene.id)).unwrap()
+    }
+  }
+  const { gotoScene, editScene } = useSceneLinks(scene)
+
+  const renderActions = [
+    <PaperClipOutlined key="query" translate={undefined} onClick={gotoScene} />,
+    <SettingOutlined key="setting" translate={undefined} onClick={() => setEditIng(true)} />,
+    <DeleteOutlined key="delete" translate={undefined} onClick={deleteHandler} />,
+  ]
+
+  if (scene.type !== SceneType.LASER) {
+    renderActions.unshift(
+      <EditOutlined key="edit" translate={undefined} onClick={editScene} />,
+    )
+  }
+
+  return (
+    <>
+      {editIng && <EditScene onClose={() => setEditIng(false)} {...scene} />}
+      <Col span={6} className={style.scene}>
+        <Card
+          bordered={true}
+          cover={<img alt={scene.cover} src={scene.cover} />}
+          actions={renderActions}>
+          <Meta
+            avatar={<Avatar src="https://joeschmoe.io/api/v1/random" />}
+            title={scene.title}
+            description={`创建时间: ${timeAgo}`}
+          />
+        </Card>
+      </Col>
+    </>
+  )
+}

+ 9 - 0
src/page/scene/style.module.css

@@ -0,0 +1,9 @@
+.header {
+  position: absolute;
+  right: 0;
+  top: 0;
+}
+
+.scene {
+  margin-bottom: 16px;
+}

+ 28 - 7
src/router.config.ts

@@ -1,41 +1,61 @@
 import { createHashHistory } from 'history'
 
-interface RouteItem {
+export interface RouteItem {
   title: string,
   path: string,
   navigation:boolean,
   icon?: string,
   parent?: RouteItem,
-  component: any
+  component: any,
+  joinTab: boolean
 }
 export const history = createHashHistory()
 
 const config: Array<RouteItem> = [
   {
+    title: '',
+    path: '/login',
+    joinTab: false,
+    navigation: false,
+    component: require('./page/Login').default
+  },
+  {
+    title: '场景管理',
+    path: '/scene',
+    joinTab: false,
+    navigation: false,
+    icon: require('./assets/images/icon_sys03.png'),
+    component: require('./page/scene/list').SceneList
+  },
+  {
     title: '栅格数据',
-    path: '/grid',
+    path: '/:sid/grid',
     navigation: true,
+    joinTab: true,
     icon: require('./assets/images/icon_sys01.png'),
     component: require('./page/List/ResterList').default
   },
   {
     title: '3D模型',
-    path: '/model',
+    path: '/:sid/model',
+    joinTab: true,
     navigation: true,
     icon: require('./assets/images/icon_sys02.png'),
     component: require('./page/List/ModelList').default
   },
   {
     title: '地形数据',
-    path: '/terrain',
+    path: '/:sid/terrain',
     navigation: true,
+    joinTab: true,
     icon: require('./assets/images/icon_sys03.png'),
     component: require('./page/List/Terrain').default
   },
   {
     title: '矢量数据',
-    path: '/gis',
+    path: '/:sid/gis',
     navigation: true,
+    joinTab: true,
     icon: require('./assets/images/icon_sys03.png'),
     component: require('./page/List/GeoList').default
   }
@@ -43,8 +63,9 @@ const config: Array<RouteItem> = [
 
 config.push({
   title: '编辑样式',
-  path: '/style/:id',
+  path: '/:sid/style/:id',
   navigation: false,
+  joinTab: true,
   component: require('./page/StyleEdit').default,
   parent: config[2]
 })

+ 13 - 1
src/store/index.ts

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

+ 110 - 0
src/store/scene.ts

@@ -0,0 +1,110 @@
+import { 
+  createSlice,
+  PayloadAction,
+  createAsyncThunk,
+  nanoid
+} from '@reduxjs/toolkit'
+import { StoreState } from '.'
+import axios from '../http'
+
+export const enum SceneType {
+  KANKAN,
+  LASER,
+  GIS
+}
+
+export interface Scene {
+  id: string,
+  title: string,
+  cover: string,
+  time: string,
+  type: SceneType,
+  m?: string
+}
+export type EditableScene = Omit<Scene, 'id' | 'time'>
+
+export type Scenes = Scene[]
+
+export const addScene = createAsyncThunk('scenes/addScene', async (scene: EditableScene) => {
+  return {
+    id: nanoid(),
+    ...scene,
+    time: new Date().toISOString()
+  }
+  // const response = await axios.post('/scenes/addScene', scene)
+  // return response.data
+})
+export const updateScene = createAsyncThunk('scenes/updateScene', async (scene: Scene) => {
+  console.log(scene)
+  return { ...scene }
+  // const response = await axios.post('/scenes/updateScene', scene)
+  // return response.data
+})
+export const deleteScene = createAsyncThunk('scenes/delScene', async (sceneId: Scene['id']) => {
+  await axios.post('/scenes/updateScene', sceneId)
+  return sceneId
+})
+
+const initialValue: Scenes = [
+  {
+    id: '1',
+    title: '长江街',
+    type: SceneType.GIS,
+    cover: 'https://4dkk.4dage.com/scene_edit_data/t-9ZwSJ5D/user/thumb-1k.jpg?v=0&rnd=0.555535123990389&x-oss-process=image/resize,m_fill,w_80,h_60/quality,q_70&rnd=0.2542055708659596',
+    time: new Date().toISOString()
+  },
+  {
+    id: '2',
+    title: '长江街',
+    type: SceneType.GIS,
+    cover: 'https://4dkk.4dage.com/scene_edit_data/t-9ZwSJ5D/user/thumb-1k.jpg?v=0&rnd=0.555535123990389&x-oss-process=image/resize,m_fill,w_80,h_60/quality,q_70&rnd=0.2542055708659596',
+    time: new Date().toISOString()
+  },
+  {
+    id: '3',
+    title: '看看-长江街',
+    type: SceneType.KANKAN,
+    m: 'KK-fKpK9EJbRU',
+    cover: 'https://4dkk.4dage.com/scene_edit_data/t-9ZwSJ5D/user/thumb-1k.jpg?v=0&rnd=0.555535123990389&x-oss-process=image/resize,m_fill,w_80,h_60/quality,q_70&rnd=0.2542055708659596',
+    time: new Date().toISOString()
+  },
+  {
+    id: '4',
+    title: '激光-长江街',
+    type: SceneType.LASER,
+    m: '3o5II3n9Rs',
+    cover: 'https://4dkk.4dage.com/scene_edit_data/t-9ZwSJ5D/user/thumb-1k.jpg?v=0&rnd=0.555535123990389&x-oss-process=image/resize,m_fill,w_80,h_60/quality,q_70&rnd=0.2542055708659596',
+    time: new Date().toISOString()
+  },
+]
+
+const sceneSlice = createSlice({
+  initialState: {
+    value: initialValue
+  },
+  name: 'scenes',
+  reducers: {
+  },
+  extraReducers(builder) {
+    builder
+      .addCase(addScene.fulfilled, (state, action: PayloadAction<Scene>) => {
+        state.value.push(action.payload)
+      })
+      .addCase(updateScene.fulfilled, (state, action: PayloadAction<Scene>) => {
+        const updateScene = state.value.find(post => post.id === action.payload.id)
+        Object.assign(updateScene, action.payload)
+      })
+      .addCase(deleteScene.fulfilled, (state, action: PayloadAction<Scene['id']>) => {
+        const index = state.value.findIndex(scene => scene.id === action.payload)
+        if (~index) {
+          state.value.splice(index, 1)
+        }
+      })
+  }
+})
+
+export const sceneReducer = sceneSlice.reducer
+
+export const scenesSelector = (state: StoreState) => state.scenes.value
+export const sceneSelector = (state: StoreState, id: Scene['id']) => 
+  state.scenes.value.find(scene => scene.id === id)

+ 10 - 0
src/style.css

@@ -54,6 +54,7 @@ html, body, #root, .app {
   margin: 22px 48px;
   display: flex;
   flex-direction: column;
+  position: relative;
 }
 
 .slide {
@@ -78,4 +79,13 @@ input {
 
 h1, h2, h3, h4, h5, h6 {
   color: inherit;
+}
+
+.ant-empty-normal {
+  color: rgba(255, 255, 255, 0.25);;
+}
+
+.ant-card-cover img {
+  height: 300px;
+  object-fit: cover;
 }

+ 11 - 0
src/util/index.ts

@@ -0,0 +1,11 @@
+export const fullRouteParams = <T extends string>(path: T, params: object) => {
+  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
+}
+

+ 5 - 5
yarn.lock

@@ -3889,7 +3889,7 @@ data-urls@^1.0.0, data-urls@^1.1.0:
     whatwg-mimetype "^2.2.0"
     whatwg-url "^7.0.0"
 
-date-fns@2.x:
+date-fns@2.x, date-fns@^2.28.0:
   version "2.28.0"
   resolved "https://registry.npmmirror.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
   integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
@@ -11183,10 +11183,10 @@ typedarray@^0.0.6:
   resolved "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
 
-typescript@4.4.4:
-  version "4.4.4"
-  resolved "https://registry.npmmirror.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
-  integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
+typescript@^4.7.4:
+  version "4.7.4"
+  resolved "https://registry.npmmirror.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
+  integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
 
 uglify-js@3.4.x:
   version "3.4.10"