Browse Source

fix: 热点添加

bill 3 years ago
parent
commit
d2dcaec82f

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+  "typescript.tsdk": "node_modules/typescript/lib"
+}

BIN
build.zip


BIN
cesium.zip


+ 6 - 2
package.json

@@ -3,6 +3,7 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@reduxjs/toolkit": "^1.8.3",
     "@types/cesium": "^1.59.3",
     "@types/jest": "24.0.23",
     "@types/node": "12.12.9",
@@ -12,8 +13,9 @@
     "@types/react-dom": "16.9.4",
     "@types/react-router": "^5.1.3",
     "@types/react-router-dom": "^5.1.3",
+    "antd": "^4.21.5",
     "axios": "^0.19.0",
-    "cesium": "^1.63.1",
+    "cesium": "1.64.0",
     "copy-webpack-plugin": "^5.0.5",
     "history": "^4.10.1",
     "ol": "4.3.3",
@@ -21,10 +23,12 @@
     "react-app-rewired": "^2.1.5",
     "react-color": "^2.17.3",
     "react-dom": "^16.12.0",
+    "react-redux": "^8.0.2",
     "react-router": "^5.1.2",
     "react-router-dom": "^5.1.2",
     "react-scripts": "3.2.0",
-    "typescript": "3.7.2"
+    "redux": "^4.2.0",
+    "typescript": "4.4.4"
   },
   "scripts": {
     "start": "react-app-rewired start",

+ 3 - 2
public/index.html

@@ -2,7 +2,6 @@
 <html lang="en">
   <head>
     <meta charset="utf-8" />
-    <link rel="stylesheet" href="%PUBLIC_URL%/style.css">
     <link rel="stylesheet" href="%PUBLIC_URL%/static/css/CesiumViewer.css">
     <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -27,6 +26,8 @@
       Learn how to configure a non-root public URL by running `npm run build`.
     -->
     <link rel="stylesheet" href="%PUBLIC_URL%/font/css/commit.css">
+    <script src="%PUBLIC_URL%/static/Cesium/Cesium.js"></script>
+    <script src="%PUBLIC_URL%/static/js/ol.js"></script>
     <title>文件管理系统</title>
   </head>
   <body>
@@ -44,6 +45,6 @@
       To create a production bundle, use `npm run build` or `yarn build`.
     -->
 
-    <script src="%PUBLIC_URL%/static/js/ol.js"></script>
+    <!-- <link rel="stylesheet" href="%PUBLIC_URL%/style.css"> -->
   </body>
 </html>

+ 2 - 0
public/style.css

@@ -52,6 +52,8 @@ html, body, #root, .app {
 .main {
   flex: 1;
   margin: 22px 48px;
+  display: flex;
+  flex-direction: column;
 }
 
 .slide {

+ 1 - 1
src/components/Upload/index.module.css

@@ -6,7 +6,7 @@
 }
 
 .filebutton input {
-  display: none;
+  display: none !important;
 }
 
 .filebutton span {

+ 25 - 18
src/components/Upload/index.tsx

@@ -54,7 +54,7 @@ async function uploadFile({ url, file, body, cb, check }: { url: string, file: A
 
 interface ItemCtrlProps extends UFile {
   api: string,
-  handleUpSuccess: Function,
+  handleUpSuccess?: (file: File) => void,
   multiple?: boolean,
   body: Object,
   check?: string,
@@ -75,25 +75,25 @@ function ItemCtrl(props: ItemCtrlProps) : ReactElement {
         setPercentage(speed)
         if (speed === 100) {
           setStatus(UPSUCCESS)
-          props.handleUpSuccess()
+          props.handleUpSuccess && props.handleUpSuccess(null as any)
         } else if (err) {
           setStatus(UPERR)
         }
       }
     })
   }
-  
   return <Item {...props} status={status} percentage={percentage} closeHandle={props.closeHandle} />
 }
 
 interface UploadProps {
   multiple?: boolean,
   className?: string,
-  api: string,
-  upHandle: Function,
+  api?: string,
+  upHandle?: (file: File) => void,
   body?: Object | null,
   check?: string,
   filter?: Function
+  text?: string
 }
 
 function Upload(props: UploadProps) : ReactElement {
@@ -135,6 +135,7 @@ function Upload(props: UploadProps) : ReactElement {
     setFlist([])
     setTimeout(() => setFlist([newFile]), 500)
     inputfile.current.value = null
+    props.upHandle && props.upHandle(newFile.file[0])
   }
 
   const closeHandle = (file:any) => {
@@ -142,25 +143,31 @@ function Upload(props: UploadProps) : ReactElement {
     fl.splice(fl.indexOf(file), 1)
     setFlist(fl)
   }
+  const MetaList = props.api
+    ? (
+          <List data={flist} className="uplist">
+          {(file: UFile, i: number, s: string) => (
+            
+            <ItemCtrl
+              closeHandle={() => closeHandle(file) }
+              handleUpSuccess={props.upHandle}
+              body={props.body ? props.body: {}}
+              api={props.api as string} {...file}
+              multiple={props.multiple}
+              check={props.check}
+              key={i} />
+          )}
+        </List>
+      )
+    : null
 
   return (
     <div className={props.className}>
       <div className={styles.filebutton}>
         <input type="file" ref={inputfile} multiple={props.multiple} onChange={onChangeHandle}/>
-        <span onClick={onClickHandle}>点击上传</span>
+        <span onClick={onClickHandle}>{props.text || '点击上传'}</span>
       </div>
-      <List data={flist} className="uplist">
-        {(file: UFile, i: number, s: string) => (
-          <ItemCtrl
-            closeHandle={() => closeHandle(file) }
-            handleUpSuccess={props.upHandle}
-            body={props.body ? props.body: {}}
-            api={props.api} {...file}
-            multiple={props.multiple}
-            check={props.check}
-            key={i} />
-        )}
-      </List>
+      {MetaList}
     </div>
   )
 }

+ 23 - 0
src/components/VectorShow/help.ts

@@ -0,0 +1,23 @@
+import * as Cesium from 'cesium'
+
+export type Pos2D = {x: number, y: number}
+export type Pos3D = {x: number, y: number, z: number}
+
+export const toScreen = (viewer: any, pos: Pos3D) => {
+  const cartesian3 = new Cesium.Cartesian3(pos.x, pos.y, pos.z)
+  const data = Cesium.SceneTransforms.wgs84ToWindowCoordinates(viewer.scene, cartesian3);
+  return {
+    x: data.x,
+    y: data.y
+  }
+}
+
+export const toReal = (viewer: any, pos: Pos2D): Pos3D => {
+  var pick1= new Cesium.Cartesian2(pos.x, pos.y);
+  var cartesian = viewer.scene.globe.pick(viewer.camera.getPickRay(pick1),viewer.scene);
+  return {
+    x: cartesian.x,
+    y: cartesian.y,
+    z: cartesian.z,
+  }
+}

+ 85 - 8
src/components/VectorShow/index.tsx

@@ -1,12 +1,19 @@
-import React from 'react'
+import React, { createRef, RefObject } from 'react'
 import { Color } from "csstype"
 import * as Cesium from 'cesium'
 import createMVTWithStyle from './mvt'
 import style from './style.module.css'
+import { AuxMenus, Menus } from '../auxMenu'
+import { Hot, Hots } from '../../store/hots'
+import { AddHot } from '../../page/hot/add'
+import { ShowHot } from '../../page/hot/show'
+import { toReal, toScreen, Pos2D } from './help'
+
 
 declare global {
   interface Window { CESIUM_BASE_URL: any; ol: any }
 }
+// Cesium.defineProperties
 
 window.CESIUM_BASE_URL = './'
 
@@ -19,6 +26,7 @@ export interface LayerStyle {
 }
 
 interface Props extends LayerStyle {
+  hots: Hots
   url: string,
   lng: number,
   lat: number,
@@ -27,7 +35,12 @@ interface Props extends LayerStyle {
 }
 
 interface State {
-  viewer: any
+  viewer: any,
+  height: number,
+  mouseEvent:  React.MouseEvent | null,
+  menus: Menus<string>,
+  hotPos: Hot['pos'] | null,
+  hotScreens: Pos2D[]
 }
 
 function createStyle (style: LayerStyle) {
@@ -46,14 +59,24 @@ function createStyle (style: LayerStyle) {
 
 class VectorView extends React.Component<Props, State> {
   clipLayer: any
+  domRef: RefObject<HTMLDivElement>
+  downHandler: () => void
 
   constructor(props: Props) {
     super(props)
-    this.state = { viewer: null }
+    this.state = { 
+      hotScreens: [],
+      hotPos: null,
+      viewer: null, 
+      height: 600,
+      mouseEvent: null,
+      menus: [{label: '添加热点', value: 'add-hot'}]
+    }
+    this.domRef = createRef()
   }
 
-  componentDidUpdate () {
-    if (!this.state.viewer) return;
+  componentDidUpdate (prevProps: Props) {
+    if (!this.state.viewer || prevProps.url === this.props.url) return;
     if (this.clipLayer) {
       this.state.viewer.imageryLayers.remove(this.clipLayer, true)
     }
@@ -65,8 +88,8 @@ class VectorView extends React.Component<Props, State> {
     }) as any;
 
     this.clipLayer = this.state.viewer.imageryLayers.addImageryProvider(layer);
+    this.updateScreens()
   }
-
   componentDidMount () {
     Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI0OGQ5Y2FiZS1iNzlmLTQyNGYtYjRkMy05ODYwY2QxZjYwYTciLCJpZCI6MjE0MTQsInNjb3BlcyI6WyJhc3IiLCJnYyJdLCJpYXQiOjE1Nzk1MzIwNjZ9.gcE0m9nus9WyfTvUw75j7-Mb9cuIFJnr7XHOVyTdTEg'
     let viewer = new Cesium.Viewer('cesiumContainer', {
@@ -83,7 +106,30 @@ class VectorView extends React.Component<Props, State> {
       destination: Cesium.Cartesian3.fromDegrees(this.props.lng, this.props.lat, this.props.height)
     });
     viewer.scene.globe.baseColor = new Cesium.Color(1.0, 1.0, 1.0, 1.0);
-    this.setState({ viewer })
+    this.setState({ 
+      viewer,
+      height: this.domRef.current?.parentElement?.offsetHeight as number - 48
+    })
+    const handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas);
+    const moveHandler = this.updateScreens.bind(this)
+
+
+    // 鼠标左键点击事件
+    handler.setInputAction((event) => {
+      // 鼠标移动事件
+      handler.setInputAction(moveHandler, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
+      handler.setInputAction(() => {
+        handler.removeInputAction(Cesium.ScreenSpaceEventType.MOUSE_MOVE);
+        handler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_UP);
+      }, Cesium.ScreenSpaceEventType.LEFT_UP);
+    }, Cesium.ScreenSpaceEventType.LEFT_DOWN);
+    handler.setInputAction(moveHandler, Cesium.ScreenSpaceEventType.WHEEL)
+    moveHandler()
+  }
+
+  updateScreens() {
+    const screens = this.props.hots.map(item => toScreen(this.state.viewer, item.pos))
+    this.setState({ hotScreens: screens })
   }
 
   componentWillUnmount() {
@@ -94,7 +140,38 @@ class VectorView extends React.Component<Props, State> {
   }
 
   render() {
-    return <div id="cesiumContainer" className={style.layer}></div>
+    const hotEles = this.props.hots.map((hot, i) => {
+      if (this.state.hotScreens[i]) {
+        return <ShowHot {...hot} key={hot.id} left={this.state.hotScreens[i].x} top={this.state.hotScreens[i].y} />
+      } else {
+        return null
+      }
+    })
+    if (this.state.hotScreens.length !== this.props.hots.length) {
+      this.updateScreens()
+    }
+
+    return (
+      <div 
+        className={style.layerParent}
+        onAuxClick={ev => {
+          ev.persist()
+          this.setState({ mouseEvent: ev })
+        }}
+        style={{height: this.state.height + 'px'}} 
+        ref={this.domRef} 
+      >
+        <div id="cesiumContainer" className={style.layer}></div>
+        <AuxMenus 
+          onExit={() => this.setState({ mouseEvent: null })}
+          menus={this.state.menus} 
+          event={this.state.mouseEvent} 
+          onSelect={(_, pos) => this.setState({ hotPos: toReal(this.state.viewer, pos) })} 
+        />
+        {this.state.hotPos && <AddHot pos={this.state.hotPos} onClose={() => this.setState({hotPos: null})} />}
+        {hotEles}
+      </div>
+    )
   }
 }
 

+ 1 - 0
src/components/VectorShow/mvt.js

@@ -28,6 +28,7 @@ function createMVTWithStyle(Cesium, ol, options) {
     this._cacheSize = 1000;
   }
 
+  console.log(Cesium.VERSION)
   Cesium.defineProperties(MVTProvider.prototype, {
     proxy: {
       get: function () {

+ 8 - 1
src/components/VectorShow/style.module.css

@@ -1,4 +1,11 @@
+.layerParent {
+  position: relative;
+  overflow: hidden;
+
+}
+
 .layer {
   width: 100%;
-  height: 600px;
+  height: 100%;
+  overflow: hidden;
 }

+ 53 - 0
src/components/auxMenu/index.tsx

@@ -0,0 +1,53 @@
+import { Menu } from 'antd'
+import React from 'react'
+import style from './style.module.css'
+
+export type MenuItem<T> = { label: string, value: T, key?: any }
+export type Menus<T> = MenuItem<T>[]
+export type Position = { x: number, y: number }
+export type props<T> = {
+  event: React.MouseEvent | null,
+  menus: Menus<T>,
+  onExit: () => void
+  onSelect: (value: T, pos: Position) => void
+}
+
+export function AuxMenus<T>(props: props<T>) {
+  if (!props.event) {
+    return null
+  }
+  const selectHandler = (item: MenuItem<T>, pos) => {
+    props.onSelect(item.value, pos)
+    props.onExit()
+  }
+
+  const pos = {
+    x: props.event.nativeEvent.offsetX,
+    y: props.event.nativeEvent.offsetY
+  }
+  const items = props.menus.map((item, index) => ({
+    key: index,
+    label: (
+      <span onClick={() => selectHandler(item, pos)}>
+        {item.label}
+      </span>
+    )
+  }))
+
+  return (
+    <div 
+      onClick={ev => ev.stopPropagation()}
+      style={{left: pos.x + 'px', top: pos.y + 'px'}} 
+      className={style['menu-layer']}>
+      <Menu
+        style={{ width: 256 }}
+        defaultSelectedKeys={['1']}
+        defaultOpenKeys={['sub1']}
+        mode="inline"
+        items={items}
+      />
+    </div>
+  )
+}
+
+export default AuxMenus

+ 3 - 0
src/components/auxMenu/style.module.css

@@ -0,0 +1,3 @@
+.menu-layer {
+  position: absolute;
+}

+ 18 - 13
src/index.tsx

@@ -1,5 +1,7 @@
 import React, {useState} from 'react';
 import ReactDOM from 'react-dom';
+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'
@@ -8,10 +10,11 @@ import Slide from './layout/Slide';
 import Combination from './layout/Combination'
 import { RouteComponentProps, Switch, Redirect } from 'react-router'
 import Login from './page/Login'
-
+import {Provider} from 'react-redux'
+import store from './store'
 
 function App() {
-  let [route, setRoute] = useState()
+  let [route] = useState<any>()
   
   let Items = config.map(item => (
     <Route
@@ -23,7 +26,7 @@ function App() {
           real={route}
           layer={item.component}
           className="main"
-          currentRoute={(r: RouteComponentProps) => (route && r.match.path === route.match.path) || setRoute(r) } />
+          currentRoute={(r: RouteComponentProps) => (route && r.match.path === route.match.path) } />
       )}
     />
   ))
@@ -31,17 +34,19 @@ function App() {
   return (
     <Router history={history}>
       <Switch>
-        <Route path="/login" component={Login} />
-        <div className="app">
-          <Header className='header' />
-          <div className='section'>
-            <Route path="/" component={() => <Slide {...route} className='slide' />} />
-            <Switch>
-              {Items}
-              <Redirect to="/gis" />
-            </Switch>
+        <Provider store={store}>
+          <Route path="/login" component={Login} />
+          <div className="app">
+            <Header className='header' />
+            <div className='section'>
+              <Route path="/" component={() => <Slide {...route} className='slide' />} />
+              <Switch>
+                {Items}
+                {/* <Redirect to="/gis" /> */}
+              </Switch>
+            </div>
           </div>
-        </div>
+        </Provider>
       </Switch>
     </Router>
   )

+ 1 - 1
src/layout/Slide.tsx

@@ -23,7 +23,7 @@ function Slide(props: RouteProps) {
     <div className={style.slidelayer + ' ' + props.className}>
       {router.map(item => (
         <Link to={item.path} key={item.path} className={query(item, currItem) ? style.active : ''} >
-          <img src={item.icon} />
+          <img src={item.icon} alt={item.icon} />
           {item.title}
         </Link>
       ))}

+ 1 - 1
src/page/List/ListState/action.ts

@@ -53,7 +53,7 @@ function getStateLocal(item: any) {
 
 export const getListAction = async (dispatch: Function, url: string, current: number) => {
   let res = await http.post(url, { "pageNum": current - 1, "pageSize": 10})
-  let list = res.data.data.content
+  let list = res.data?.data?.content || []
 
   list = list.map((item: any) => ({
     ...item,

+ 4 - 3
src/page/List/grent.tsx

@@ -1,4 +1,4 @@
-import React, { useReducer, useEffect, useState, Fragment } from 'react'
+import React, { useReducer, useState, Fragment, useCallback, useEffect } from 'react'
 import List from '../../components/List'
 import Item from '../../components/item'
 import Paging from '../../components/Paging'
@@ -55,13 +55,14 @@ interface GrentApi {
   region?: boolean
 }
 
+const useEffectRaw = useEffect
 export default function GrentReducer1({ getUrl, delUrl, zipUrl, sectionUrl, transformUrl, transferUrl, judgeUrl, ItemFn, region }: GrentApi) {
   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 = (current: number) => getListAction(modelDispatch, getUrl, current)
+  const updateAction = useCallback((current: number) => getListAction(modelDispatch, getUrl, current), [getUrl])
   const referData = () => setReferCount(++referCount)
   const delHandle = async (model: Model) => {
     await delItemAction(modelDispatch, delUrl + model.id + '/', model)
@@ -74,7 +75,7 @@ export default function GrentReducer1({ getUrl, delUrl, zipUrl, sectionUrl, tran
   }
 
   // updateAction(modelState.paging.current)
-  useEffect(() => { updateAction(modelState.paging.current) }, [referCount])
+  useEffectRaw(() => { updateAction(modelState.paging.current) }, [referCount])
 
   const Element = (
     <ListItems

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

@@ -31,9 +31,8 @@ function Login() {
         <label htmlFor="psw">密码</label>
         <input type="password" name="psw" value={psw} onChange={ev => setPsw(ev.target.value)}/>
       </div>
-
       <div>
-        <a className={style.button} onClick={submission}>登录</a>
+        <span className={style.button} onClick={submission}>登录</span>
       </div>
     </div>
   )

+ 13 - 5
src/page/StyleEdit/index.tsx

@@ -4,6 +4,9 @@ import { reducer, initialState, getLayersAction, updateLayerAction, saveLayersAc
 import VectorShow from '../../components/VectorShow'
 import Color from '../../components/Color'
 import styles from './style.module.css'
+import { useSelector } from 'react-redux'
+import { StoreState } from '../../store'
+import { Hots } from '../../store/hots'
 
 type Attr = 'lineColor' | 'lineWidth' | 'fillColor' | 'show'
 type EvAttr = 'value' | 'checked'
@@ -14,6 +17,7 @@ function StyleEdit(props: RouteComponentProps) {
   let [layer, setLayer] = useState('')
   let params = props.match.params as any
   let style = (layer && state[layer]) as Item
+  const hots = useSelector<StoreState, Hots>(state => state.hots)
 
   useEffect(() => { getLayersAction(dispatch, params.id) }, [params.id])
   useEffect(() => { layer || setLayer(layers[0]) }, [layer, layers])
@@ -21,17 +25,17 @@ function StyleEdit(props: RouteComponentProps) {
   const changeHandle = (attr: Attr, evAttr: EvAttr = 'value', init = false) => (ev: any) => {
     updateLayerAction(dispatch, layer, { ...style, [attr]: init ? ev : ev.target[evAttr] })
   }
-  const saveHandle = async () => {{
+  const saveHandle = async () => {
     if (await saveLayersAction(style, params.id)) {
       alert('成功发布')
     } else {
       alert('发布失败')
     }
-  }}
+  }
   const verctorInfo = style && { ...style, url: style.getUrl }
 
   return (
-    <div>
+    <div className={styles.layout}>
       <div className={styles.filter}>
         <div className={styles.inputitem}>
           <label>图层选择</label>
@@ -58,12 +62,16 @@ function StyleEdit(props: RouteComponentProps) {
               <input type="checkbox" checked={style.show} onChange={changeHandle('show', 'checked')} />
             </div>
             <div className={styles.inputitem}>
-              <button onClick={saveHandle}>发布</button>
+              <button onClick={saveHandle} >发布</button>
             </div>
             </Fragment>
         )}
       </div>
-      {style && <div className={styles.content}><VectorShow {...verctorInfo} height={2500} /></div>}
+      {style && 
+        <div className={styles.content}>
+          <VectorShow {...verctorInfo} height={2500} hots={hots} />
+        </div>
+      }
     </div>
   )
 }

+ 7 - 0
src/page/StyleEdit/style.module.css

@@ -56,4 +56,11 @@
   padding: 24px;
   border-radius: 3px;
   margin-top: 15px;
+  flex: 1;
+}
+
+.layout {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
 }

+ 79 - 0
src/page/hot/add.tsx

@@ -0,0 +1,79 @@
+import React, {useMemo, useState} from 'react'
+import { HotModal } from './component'
+import { Hot, hotAdded } from '../../store/hots'
+import { nanoid } from '@reduxjs/toolkit'
+import { useDispatch } from 'react-redux'
+import { Input } from 'antd'
+import Upload from '../../components/Upload'
+import style from './style.module.css'
+import { useForm } from 'antd/lib/form/Form'
+
+export type Props = {
+  onClose: () => void,
+  pos: Hot['pos']
+}
+
+export function AddHot(props: Props) {
+  const [file, setFile] = useState<File>()
+  const [form] = useForm<Hot>()
+  const dispatch = useDispatch()
+  const meta = useMemo(() => {
+    if (file) {
+      return {
+        name: file.name,
+        type: file.type.substring(0, file.type.indexOf('/')),
+        url: URL.createObjectURL(file)
+      }
+    }
+  }, [file])
+  const metaEle = {
+    'image': <img src={meta?.url} alt={meta?.name} />
+  }
+  
+  const items = [
+    {
+      label: '请输入热点标题',
+      ele: <Input />,
+      name: 'title',
+    },
+    {
+      label: '请输入热点内容',
+      ele: <Input.TextArea />,
+      name: 'content',
+    },
+    {
+      label: '请输入上传照片或视频',
+      ele: (
+        <div className={style.meta}>
+          <Upload upHandle={setFile} text={`点击${file ? '替换' : '上传'}`} />
+          {meta?.type && metaEle[meta.type]}
+        </div>
+      )
+    },
+    {
+      label: '请输入外链',
+      ele: <Input />,
+      name: 'link',
+    },
+  ]
+
+  const onSubmit = () => {
+    dispatch(
+      hotAdded({
+        ...form.getFieldsValue(),
+        pos: props.pos,
+        meta: meta,
+        id: nanoid()
+      })
+    )
+    props.onClose()
+  }
+
+  return <HotModal 
+    title="添加热点" 
+    items={items} 
+    form={form}
+    enter={onSubmit} 
+    cancel={props.onClose}
+  />
+}

+ 45 - 0
src/page/hot/component.tsx

@@ -0,0 +1,45 @@
+import { Modal, Form, FormProps, FormInstance } from 'antd'
+import React from 'react'
+import { Hot } from '../../store/hots';
+
+export type Item = {
+  label?: string, 
+  ele: React.ReactElement,
+  name?: string
+}
+export type Props = {
+  title: string,
+  items: Item[],
+  width?: number
+  form?: FormInstance<Hot>
+  onValueChange?: FormProps['onValuesChange']
+  cancel?: () => void,
+  enter?: () => void,
+}
+
+export function HotModal(props: Props) {
+  const FormItems = props.items.map(({label, name, ele}, index) => (
+    <Form.Item label={label} name={name} key={name || index}>
+      {ele}
+    </Form.Item>
+  ))
+
+  return (
+    <Modal 
+      width={props.width}
+      title={props.title} 
+      visible={true} 
+      onOk={props.enter} 
+      okText='确定'
+      cancelText='取消'
+      onCancel={props.cancel}>
+      <Form
+        form={props.form}
+        layout="vertical"
+        onValuesChange={props.onValueChange}
+      >
+        {FormItems}
+      </Form>
+    </Modal>
+  )
+}

+ 57 - 0
src/page/hot/show.tsx

@@ -0,0 +1,57 @@
+import React, { useState } from 'react'
+import { Hot } from '../../store/hots';
+import style from './style.module.css'
+import { HotModal, Item } from './component'
+import { Image } from 'antd'
+
+export type Props = Hot & { left: number, top: number }
+
+export function ShowHot(props: Props) {
+  const [showModal, setShowModal] = useState(false)
+  const inlineStyle = {
+    left: props.left + 'px',
+    top: props.top + 'px'
+  }
+  const metaEle = {
+    'image': <Image src={props.meta?.url} alt={props.meta?.type} />
+  }
+  const items: Item[] = []
+  let width = 520
+
+  if (props.link) {
+    width = 980
+    items.push({
+      ele: <iframe src={props.link} title={props.title} className={style['iframe']} />
+    })
+  } else {
+    if (props.meta) {
+      items.push({
+        label: '多媒体',
+        ele: metaEle[props.meta.type]
+      })
+    }
+    if (props.content) {
+      items.push({
+        label: '热点内容',
+        ele: <div>{props.content}</div>
+      })
+    }
+  }
+
+  return (
+    <>
+      <div style={inlineStyle} className={style['show-hot']} onClick={() => setShowModal(true)}>
+        <img src="https://laser-oss.4dkankan.com/icon/lQLPDhrvVzvNvTswMLAOU-UNqYnnZQG1YPJUwLwA_48_48.png" alt="label" />
+      </div>
+      {showModal && 
+        <HotModal 
+          width={width} 
+          title={props.title} 
+          items={items} 
+          enter={() => setShowModal(false)} 
+          cancel={() => setShowModal(false)} 
+        /> 
+      }
+    </>
+  )
+}

+ 20 - 0
src/page/hot/style.module.css

@@ -0,0 +1,20 @@
+.meta img {
+  max-width: 100%;
+}
+
+.show-hot {
+  position: absolute;
+  transform: translate(-50%, -50%);
+  width: 24px;
+  cursor: pointer;
+}
+
+.show-hot > * {
+  max-width: 100%;
+}
+
+.iframe {
+  width: 100%;
+  height: 600px;
+  border: none;
+}

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

@@ -1 +1,3 @@
 /// <reference types="react-scripts" />
+
+declare module 'cesium';

+ 2 - 2
src/setupProxy.js

@@ -4,7 +4,7 @@ module.exports = app => {
   app.use(
     proxy('/api', {
       // target: 'http://192.168.0.10:8082',
-      target: 'http://47.107.252.54:8082',
+      target: 'http://47.112.166.173:8082',
       changeOrigin: true,
       pathRewrite: {
         "^/api": "/api"
@@ -15,7 +15,7 @@ module.exports = app => {
   app.use(
     proxy('/test', {
       // target: 'http://192.168.0.10:8082',
-      target: 'http://47.107.252.54:8082',
+      target: 'http://47.112.166.173:8082',
       changeOrigin: true,
       pathRewrite: {
         "^/test": "/"

+ 35 - 0
src/store/hots.ts

@@ -0,0 +1,35 @@
+import { createSlice } from '@reduxjs/toolkit'
+import { PayloadAction } from '@reduxjs/toolkit'
+
+export interface Hot {
+  id: string,
+  pos: { x: number, y: number, z: number }
+  title: string,
+  content?: string
+  meta: {
+    type: string,
+    url: string
+  }
+  link?: string
+}
+
+export type Hots = Hot[]
+
+const initialState: Hots = []
+
+const hotsSlice = createSlice({
+  initialState,
+  name: 'posts',
+  reducers: {
+    hotAdded(state, action: PayloadAction<Hot>) {
+      console.log('push', action.payload)
+      state.push(action.payload)
+    }
+  }
+})
+
+export const hotsReducer = hotsSlice.reducer
+
+export const { 
+  hotAdded 
+} = hotsSlice.actions

+ 11 - 0
src/store/index.ts

@@ -0,0 +1,11 @@
+import { configureStore } from '@reduxjs/toolkit'
+import { hotsReducer } from './hots'
+
+const store = configureStore({
+  reducer: {
+    hots: hotsReducer
+  }
+})
+
+export type StoreState = ReturnType<typeof store.getState>
+export default store

+ 81 - 0
src/style.css

@@ -0,0 +1,81 @@
+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;
+}
+
+h1,
+h2,
+h3 {
+  font-weight: 400;
+  margin: 0;
+  padding: 0;
+}
+
+body {
+  background-color: #121212;
+  color: #e8e8e8 ;
+}
+
+html, body, #root, .app {
+  height: 100%;
+}
+
+.app {
+  display: flex;
+  flex-direction: column;
+}
+
+.title {
+  font-size: 28px;
+  text-align: center;
+}
+
+.header {
+  flex: 0 0 auto;
+}
+
+.section {
+  flex: 1;
+  display: flex;
+  align-items: stretch
+}
+
+.main {
+  flex: 1;
+  margin: 22px 48px;
+  display: flex;
+  flex-direction: column;
+}
+
+.slide {
+  flex: 0 0 260px;
+}
+
+select,
+input {
+  margin: 0 8px;
+  background: transparent;
+  padding: 4px 10px;
+  border: 1px solid #2e2e2e;
+  outline: none;
+  color: inherit;
+  border-radius: 3px;
+}
+
+.cesium-viewer-fullscreenContainer,
+.cesium-viewer-bottom {
+  display: none !important;
+}
+
+h1, h2, h3, h4, h5, h6 {
+  color: inherit;
+}

+ 2 - 1
tsconfig.json

@@ -7,10 +7,11 @@
       "esnext"
     ],
     "allowJs": true,
+    "checkJs": false,
     "skipLibCheck": true,
     "esModuleInterop": true,
     "allowSyntheticDefaultImports": true,
-    "strict": true,
+    "strict": false,
     "forceConsistentCasingInFileNames": true,
     "module": "esnext",
     "moduleResolution": "node",

File diff suppressed because it is too large
+ 11979 - 0
yarn.lock