Browse Source

initial

v1.1.0
sean.zhou 3 years ago
parent
commit
b5f655fd6e
  1. 1
      .eslintignore
  2. 22
      .eslintrc.js
  3. 25
      .gitignore
  4. 5
      .gitignore copy
  5. 1
      .npmrc
  6. 2
      README.md
  7. 2
      env/.env
  8. 2
      env/.env.production
  9. 2
      env/.env.stag
  10. 13
      index.html
  11. 13530
      package-lock.json
  12. 87
      package.json
  13. BIN
      public/favicon.ico
  14. 34
      src/App.vue
  15. 13
      src/antd.ts
  16. 42
      src/api/http.ts
  17. 20
      src/api/http/config.ts
  18. 64
      src/api/http/request.ts
  19. 38
      src/api/http/type.ts
  20. 52
      src/api/layer.ts
  21. 62
      src/api/manage.ts
  22. 9
      src/api/media.ts
  23. 183
      src/api/pilot-bridge.ts
  24. 9
      src/api/wayline.ts
  25. 30
      src/api/websocket.ts
  26. 11
      src/assets/icons/check.svg
  27. 16
      src/assets/icons/dji-logo-vector.svg
  28. 4
      src/assets/icons/layer.svg
  29. 4
      src/assets/icons/media.svg
  30. 22
      src/assets/icons/pin-19be6b.svg
  31. 22
      src/assets/icons/pin-212121.svg
  32. 22
      src/assets/icons/pin-2d8cf0.svg
  33. 22
      src/assets/icons/pin-b620e0.svg
  34. 22
      src/assets/icons/pin-e23c39.svg
  35. 22
      src/assets/icons/pin-ffbb00.svg
  36. 14
      src/assets/icons/tsa.svg
  37. 311
      src/components/GMap.vue
  38. 217
      src/components/LayersTree.vue
  39. 92
      src/components/MediaPanel.vue
  40. 42
      src/components/svgIcon.vue
  41. 122
      src/components/wayline-panel.vue
  42. 19
      src/constants/index.ts
  43. 23
      src/constants/map.ts
  44. 138
      src/constants/mock-layers.ts
  45. 9
      src/env.d.ts
  46. 144
      src/hooks/use-g-map-cover.ts
  47. 34
      src/hooks/use-g-map.ts
  48. 71
      src/hooks/use-mouse-tool.ts
  49. 14
      src/main.ts
  50. 21
      src/pages/elements/elements.vue
  51. 150
      src/pages/page-pilot/pilot-home.vue
  52. 145
      src/pages/page-pilot/pilot-index.vue
  53. 202
      src/pages/page-pilot/pilot-liveshare.vue
  54. 130
      src/pages/page-pilot/pilot-media.vue
  55. 208
      src/pages/project-app/index.vue
  56. 438
      src/pages/project-app/projects/layer.vue
  57. 327
      src/pages/project-app/projects/livestream-agora.vue
  58. 351
      src/pages/project-app/projects/livestream-others.vue
  59. 82
      src/pages/project-app/projects/livestream.vue
  60. 11
      src/pages/project-app/projects/media.vue
  61. 11
      src/pages/project-app/projects/tsa.vue
  62. 9
      src/pages/project-app/projects/wayline.vue
  63. 103
      src/pages/project-app/sidebar.vue
  64. 50
      src/plugins/svgBuilder.ts
  65. 24
      src/root.ts
  66. 70
      src/router/index.ts
  67. 5
      src/shims-vue.d.ts
  68. 135
      src/store/index.ts
  69. 19
      src/styles/common.scss
  70. 318
      src/styles/flex.style.scss
  71. 72
      src/styles/fonts.scss
  72. 3
      src/styles/index.scss
  73. 164
      src/styles/reset.scss
  74. 72
      src/styles/variables.scss
  75. 19
      src/types/enums.ts
  76. 1
      src/types/index.ts
  77. 6
      src/types/map-enum.ts
  78. 100
      src/types/map.d.ts
  79. 98
      src/types/mapLayer.ts
  80. 13
      src/use-common-components.ts
  81. 4
      src/utils/data-process.ts
  82. 81
      src/utils/genjson.ts
  83. 19
      src/utils/layer-tree.ts
  84. 28
      src/utils/logger.ts
  85. 44
      src/utils/map-layer-utils.ts
  86. 42
      src/utils/storage.ts
  87. 6
      src/utils/uuid.ts
  88. 85
      src/vendors/coordtransform.js
  89. 2
      src/vendors/jswebrtc.min.js
  90. 1
      src/vite-env.d.ts
  91. 29
      tsconfig.json
  92. 70
      vite.config.ts
  93. 3927
      yarn.lock

1
.eslintignore

@ -0,0 +1 @@
/src/vendors/**

22
.eslintrc.js

@ -0,0 +1,22 @@
module.exports = {
env: {
browser: true,
commonjs: true,
es2021: true,
node: true
},
extends: ['standard', 'plugin:vue/vue3-essential'],
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser'
},
plugins: ['vue', '@typescript-eslint'],
rules: {
'comma-dangle': 'off',
'import/no-absolute-path': 'off',
'no-unused-vars': 'off',
camelcase: 'off',
'no-redeclare': 'off',
'vue/no-unused-components': 'off'
}
}

25
.gitignore vendored

@ -0,0 +1,25 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
node_modules/
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
# .vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.history
/coverage
/backup
node_modules

5
.gitignore copy

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

1
.npmrc

@ -0,0 +1 @@
registry=https://registry.npm.taobao.org/

2
README.md

@ -0,0 +1,2 @@
# VUE3
全系统选用 script setup 模式

2
env/.env vendored

@ -0,0 +1,2 @@
VITE_APP_ENVIRONMENT=DEV
VITE_APP_APIGATEWAY_BACKEND_HOST=''

2
env/.env.production vendored

@ -0,0 +1,2 @@
VITE_APP_ENVIRONMENT=production
VITE_APP_APIGATEWAY_BACKEND_HOST=''

2
env/.env.stag vendored

@ -0,0 +1,2 @@
VITE_APP_ENVIRONMENT=STAG
VITE_APP_APIGATEWAY_BACKEND_HOST=''

13
index.html

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>demo-web</title>
</head>
<body>
<div id="demo-app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

13530
package-lock.json generated

File diff suppressed because it is too large Load Diff

87
package.json

@ -0,0 +1,87 @@
{
"name": "demo-web",
"version": "0.0.1",
"scripts": {
"serve": "vite",
"build:test": "vite build --mode stag",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint --fix"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@ant-design/icons-vue": "^6.0.1",
"@vitejs/plugin-legacy": "^1.6.2",
"agora-rtc-sdk-ng": "latest",
"ant-design-vue": "^2.2.8",
"axios": "^0.21.1",
"query-string": "^7.0.1",
"reconnecting-websocket": "^4.4.0",
"vconsole": "^3.8.1",
"vite-plugin-components": "^0.13.3",
"vite-plugin-importer": "^0.2.5",
"vite-plugin-optimize-persist": "^0.1.2",
"vite-plugin-package-config": "^0.1.1",
"vue": "^3.2.26",
"vue-cookies": "^1.7.4",
"vue-i18n": "^9.1.6",
"vue-router": "4",
"vuex": "^4.0.2"
},
"devDependencies": {
"@types/node": "^16.3.2",
"@types/urlencode": "^1.1.2",
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"@vitejs/plugin-vue": "^1.2.4",
"@vue/compiler-sfc": "^3.0.5",
"eslint": "^7.30.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-vue": "^7.13.0",
"rollup-plugin-external-globals": "^0.6.1",
"sass": "^1.35.1",
"typescript": "^4.5.4",
"vite": "^2.4.0",
"vite-plugin-eslint": "^1.3.0",
"vite-plugin-style-import": "^1.0.1",
"vite-plugin-svg-icons": "^1.0.5",
"vite-plugin-vconsole": "^1.1.0",
"vue-tsc": "^0.0.24"
},
"license": "ISC",
"vite": {
"optimizeDeps": {
"include": [
"@amap/amap-jsapi-loader",
"@ant-design/icons-vue",
"@vue/reactivity",
"agora-rtc-sdk-ng",
"ant-design-vue",
"ant-design-vue/es",
"ant-design-vue/es/button/style/css",
"ant-design-vue/es/divider/style/css",
"ant-design-vue/es/drawer/style/css",
"ant-design-vue/es/form/style/css",
"ant-design-vue/es/image/style/css",
"ant-design-vue/es/input/style/css",
"ant-design-vue/es/message/style/css",
"ant-design-vue/es/modal/style/css",
"ant-design-vue/es/radio/style/css",
"ant-design-vue/es/select/style/css",
"ant-design-vue/es/switch/style/css",
"ant-design-vue/es/table/style/css",
"ant-design-vue/es/tooltip/style/css",
"ant-design-vue/es/tree/style/css",
"axios",
"reconnecting-websocket",
"vconsole",
"vue",
"vue-router",
"vuex"
]
}
}
}

BIN
public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

34
src/App.vue

@ -0,0 +1,34 @@
<template>
<div id="demo-app" class="demo-app">
<router-view />
<!-- <div class="map-wrapper">
<GMap/>
</div> -->
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue'
import { useMyStore } from './store'
import GMap from '/@/components/GMap.vue'
export default defineComponent({
name: 'App',
components: { GMap },
setup () {
const store = useMyStore()
return {}
}
})
</script>
<style lang="scss" scoped>
.demo-app {
width: 100%;
height: 100%;
.map-wrapper {
height: 100%;
width: 100%;
}
}
</style>

13
src/antd.ts

@ -0,0 +1,13 @@
// import Icon from '@ant-design/icons-vue'
import * as antDesign from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
import { App } from 'vue'
import svgIcon from '/@/components/svgIcon.vue'
export const antComponents = {
install (app: App): void {
app.use(antDesign)
// app.component('Icon', Icon)
app.component('svg-icon', svgIcon)
}
}

42
src/api/http.ts

@ -0,0 +1,42 @@
/**
*
* 1. axios
* 2.
*
* API
* 1. axios 实例: singleAxiosInstance
* 2. axios createAxiosInstance
* 3.允许外界进行定制: bindCommonRequestInterceptorsbindCommonResponseInterceptors
*/
import Axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
// 统一的 request 拦截器
export function bindCommonRequestInterceptors (instance: AxiosInstance): void {
instance.interceptors.request.use(config => {
return config
})
}
// Unified response interceptor
export function bindCommonResponseInterceptors (instance: AxiosInstance): void {
instance.interceptors.response.use(config => {
return config
}, err => {
return Promise.reject(err)
})
}
export function createAxiosInstance (config?: AxiosRequestConfig, commonInterceptorConf: { request?: boolean, response?: boolean } = {}): AxiosInstance {
const instance = Axios.create(config)
// Binding a unified interceptor, binding by default
commonInterceptorConf.request !== false && bindCommonRequestInterceptors(instance)
commonInterceptorConf.response !== false && bindCommonResponseInterceptors(instance)
return instance
}
const singleAxios = createAxiosInstance({}, { request: true, response: false })
export default singleAxios

20
src/api/http/config.ts

@ -0,0 +1,20 @@
export const CURRENT_CONFIG = {
baseURL: 'Please enter the backend access address prefix.', // This url must end with "/". Example: 'http://192.168.1.1:6789/cloud/'
websocketURL: 'Please enter the WebSocket access address.', // Example: 'ws://192.168.1.1:6789/api/v1/ws'
rtmpURL: 'Please enter the rtmp access address.', // Example: 'rtmp://192.168.1.1/live/'
gb28181Para:
'serverIP=Please enter the server ip.&serverPort=Please enter the server port.&serverID=Please enter the server id.' +
'&agentID=Please enter the agent id.&agentPassword=Please enter the agent password' +
'&localPort=Please enter the local port.&channel=Please enter the channel.',
rtspPara: 'userName=Please enter the username.&password=Please enter the password&port=Please enter the port.',
amapKey: 'Please enter the amap key.',
agoraAPPID: 'Please enter the agora app id.',
agoraToken: 'Please enter the agora token.',
agoraChannel: 'Please enter the agora channel.',
appId: 'Please enter the app id.', // You need to go to the development website to apply.
appKey: 'Please enter the app key.', // You need to go to the development website to apply.
appLicense: 'Please enter the app license.' // You need to go to the development website to apply.
}

64
src/api/http/request.ts

@ -0,0 +1,64 @@
import axios from 'axios'
import { uuidv4 } from '/@/utils/uuid'
import { CURRENT_CONFIG } from './config'
export * from './type'
const REQUEST_ID = 'X-Request-Id'
function getAuthToken () {
return localStorage.getItem('x-auth-token')
}
const instance = axios.create({
// withCredentials: true,
headers: {
'Content-Type': 'application/json',
},
// timeout: 12000,
})
instance.interceptors.request.use(
config => {
config.headers['X-Auth-Token'] = getAuthToken()
// config.headers[REQUEST_ID] = uuidv4()
config.baseURL = CURRENT_CONFIG.baseURL
return config
},
error => {
return Promise.reject(error)
},
)
instance.interceptors.response.use(
response => response,
err => {
const requestId = err?.config?.headers && err?.config?.headers[REQUEST_ID]
console.info('')
if (requestId) {
console.info(REQUEST_ID, ':', requestId)
}
console.info('url: ', err?.config?.url, `${err?.config?.method}\n>>>> err: `, err)
let description = '-'
if (err.response?.data && err.response.data.message) {
description = err.response.data.message
}
if (err.response?.data && err.response.data.result) {
description = err.response.data.result.message
}
// @See: https://github.com/axios/axios/issues/383
if (!err.response || !err.response.status) {
console.log('The network is abnormal, please check the network and try again')
} else if (err.response?.status !== 200) {
console.log(`ERROR_CODE: ${err.response?.status}`)
}
if (err.response?.status === 403) {
// window.location.href = '/'
}
if (err.response?.status === 401) {
console.log(err.response)
}
return Promise.reject(err)
},
)
export default instance

38
src/api/http/type.ts

@ -0,0 +1,38 @@
export interface IResult {
code: number;
message: string;
}
export interface IPage {
page: number;
total: number;
page_size: number;
}
export interface IListWorkspaceResponse<T> {
code: number;
message: string;
data: {
list: T[];
pagination: IPage;
};
}
// Workspace
export interface IWorkspaceResponse<T> {
[x: string]: number;
code: number;
data: T;
message: string;
}
export type IStatus = 'WAITING' | 'DOING' | 'SUCCESS' | 'FAILED';
export interface CommonListResponse<T> extends IResult {
data: {
list: T[];
pagination: IPage;
};
}
export interface CommonResponse<T> extends IResult {
data: T
}

52
src/api/layer.ts

@ -0,0 +1,52 @@
import request, { IWorkspaceResponse } from '/@/api/http/request'
import { mapLayers } from '/@/constants/mock-layers'
import { elementGroupsReq, PostElementsBody, PutElementsBody } from '/@/types/mapLayer'
const PREFIX = '/map/api/v1'
const workspace_id = localStorage.getItem('workspace-id')
type UnknownResponse = Promise<IWorkspaceResponse<unknown>>
// get elements group
// export const getLayers = async (reqParams: elementGroupsReq): UnknownResponse => {
// const url = `${PREFIX}/workspaces/${workspace_id}/element_groups`
// const result = await request.get(url, {
// params: {
// group_id: reqParams.groupId,
// is_distributed: reqParams.isDistributed
// },
// })
// return result.data
// }
export const getLayers = async (reqParams: elementGroupsReq): UnknownResponse => {
return mapLayers
}
// Get elements groups request
export const getElementGroupsReq = async (body: elementGroupsReq): Promise<IWorkspaceResponse<any>> => {
const url = `${PREFIX}/workspaces/` + workspace_id + '/element-groups'
const result = await request.get(url, body)
return result.data
}
// add element
export const postElementsReq = async (pid: string, body: PostElementsBody): Promise<IWorkspaceResponse<{ id: string }>> => {
const url = `${PREFIX}/workspaces/` + workspace_id + `/element-groups/${pid}/elements`
const result = await request.post(url, body)
return result.data
}
// Update map element request
export const updateElementsReq = async (id: string, body: PutElementsBody): Promise<IWorkspaceResponse<{ id: string }>> => {
const url = `${PREFIX}/workspaces/` + workspace_id + `/elements/${id}`
const result = await request.put(url, body)
return result.data
}
// Delete map element
export const deleteElementReq = async (id: string, body: {}): Promise<any> => {
const url = `${PREFIX}/workspaces/` + workspace_id + `/elements/${id}`
const result = await request.delete(url, body)
return result.data
}
// Delete layer elements
export const deleteLayerEleReq = async (id: string, body: {}): Promise<any> => {
const url = `${PREFIX}/workspaces/` + workspace_id + `/element-groups/${id}/elements`
const result = await request.delete(url, body)
return result.data
}

62
src/api/manage.ts

@ -0,0 +1,62 @@
import request, { IWorkspaceResponse } from '/@/api/http/request'
const HTTP_PREFIX = '/manage/api/v1'
// login
interface loginBody {
username: string,
password: string
}
export const login = async function (body: loginBody): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/login`
const result = await request.post(url, body)
return result.data
}
// Refresh Token
export const refreshToken = async function (body: {}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/token/refresh`
const result = await request.post(url, body)
return result.data
}
// Get Platform Info
export const getPlatformInfo = async function (body: {}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/workspaces/current`
const result = await request.get(url, body)
return result.data
}
// Get User Info
export const getUserInfo = async function (body: {}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/users/current`
const result = await request.get(url, body)
return result.data
}
// Get Device Topo
export const getDeviceTopo = async function (body: {}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/devices/devices`
const result = await request.get(url, body)
return result.data
}
// Get Livestream Capacity
export const getLiveCapacity = async function (body: {}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/live/capacity`
const result = await request.get(url, body)
return result.data
}
// Start Livestream
export const startLivestream = async function (body: {}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/live/streams/start`
const result = await request.post(url, body)
return result.data
}
// Stop Livestream
export const stopLivestream = async function (body: {}): Promise<IWorkspaceResponse<any>> {
const url = `${HTTP_PREFIX}/live/streams/stop`
const result = await request.post(url, body)
return result.data
}

9
src/api/media.ts

@ -0,0 +1,9 @@
import request from '/@/api/http/request'
const HTTP_PREFIX = '/media/api/v1'
// Get Media Files
export const getMediaFiles = async function (wid: string, body: {}): Promise<any> {
const url = `${HTTP_PREFIX}/files/${wid}/files`
const result = await request.get(url, body)
return result.data
}

183
src/api/pilot-bridge.ts

@ -0,0 +1,183 @@
import { getRoot } from '/@/root'
const root = getRoot()
const components = new Map()
declare let window:any
interface JsResponse{
code:number,
message:string,
data:{}
}
export default {
init () {
components.set('thing', {
host: '',
connectCallback: '',
username: '',
password: ''
})
components.set('liveshare', {
videoPublishType: 'video-demand-aux-manual', // video-on-demand、video-by-manual、video-demand-aux-manual
statusCallback: ''
})
components.set('map', {
userName: '',
elementPreName: ''
})
components.set('ws', {
host: '',
token: '',
connectCallback: ''
})
components.set('api', {
host: '',
token: ''
})
components.set('tsa', {
})
components.set('media', {
autoUploadPhoto: true, // 是否自动上传图片, 非必需
autoUploadPhotoType: 1, // 自动上传的照片类型,0:原图, 1:缩略图, 非必需
autoUploadVideo: true // 是否自动上传视频, 非必需
})
components.set('mission', {
})
},
getComponentParam (key:string) {
return components.get(key)
},
setComponentParam (key:string, value:any) {
components.set(key, value)
},
loadComponent (name:string, param:any):string {
return window.djiBridge.platformLoadComponent(name, JSON.stringify(param))
},
unloadComponent (name:string) :string {
return window.djiBridge.platformUnloadComponent(name)
},
isComponentLoaded (module:string):string {
return window.djiBridge.platformIsComponentLoaded(module)
},
setWorkspaceId (uuid:string):string {
return window.djiBridge.platformSetWorkspaceId(uuid)
},
setPlatformMessage (platformName:string, title:string, desc:string):string {
return window.djiBridge.platformSetInformation(platformName, title, desc)
},
getRemoteControllerSN () :string {
return window.djiBridge.platformGetRemoteControllerSN()
},
getAircraftSN ():string {
return window.djiBridge.platformGetAircraftSN()
},
stopwebview ():string {
return window.djiBridge.platformStopSelf()
},
getToken () :string {
const res:string = this.isComponentLoaded('api')
const resObj = JSON.parse(res)
console.log('api load status:', resObj)
if (resObj.data === true) {
const tokenRes = JSON.parse(window.djiBridge.apiGetToken())
return tokenRes.data
} else {
console.warn('warning: not api component loaded!!')
return ''
}
},
setToken (token:string):string {
return window.djiBridge.apiSetToken(token)
},
setLogEncryptKey (key:string):string {
return window.djiBridge.platformSetLogEncryptKey(key)
},
clearLogEncryptKey ():string {
return window.djiBridge.platformClearLogEncryptKey()
},
getLogPath ():string {
return window.djiBridge.platformGetLogPath()
},
platformVerifyLicense (appId:string, appKey:string, appLicense:string):string {
return window.djiBridge.platformVerifyLicense(appId, appKey, appLicense)
},
isPlatformVerifySuccess ():string {
return window.djiBridge.platformIsVerified()
},
// liveshare
/**
*
* @param type
* video-on-demand: 服务器点播thing模块
* video-by-manual
* video-demand-aux-manual: 混合模式
*/
setVideoPublishType (type:string):string {
return window.djiBridge.liveshareSetVideoPublishType(type)
},
/**
*
* @returns
* type: liveshare type 0unknown, 1:agora, 2:rtmp, 3:rtsp, 4:gb28181
*/
getLiveshareConfig () {
return window.djiBridge.liveshareGetConfig()
},
setLiveshareConfig (type:number, params:string):string {
return window.djiBridge.liveshareSetConfig(type, params)
},
setLiveshareStatusCallback (callbackFunc:string) :string {
return window.djiBridge.liveshareSetStatusCallback(callbackFunc)
},
getLiveshareStatus () {
return window.djiBridge.liveshareGetStatus()
},
startLiveshare ():string {
return window.djiBridge.liveshareStartLive()
},
stopLiveshare ():string {
return window.djiBridge.liveshareStopLive()
},
// media
setAutoUploadPhoto (auto:boolean):string {
return window.djiBridge.mediaSetAutoUploadPhoto(auto)
},
getAutoUploadPhoto () {
return window.djiBridge.mediaGetAutoUploadPhoto()
},
setUploadPhotoType (type:number):string {
return window.djiBridge.mediaSetUploadPhotoType(type)
},
getUploadPhotoType () {
return window.djiBridge.mediaGetUploadPhotoType()
},
setAutoUploadVideo (auto:boolean):string {
return window.djiBridge.mediaSetAutoUploadVideo(auto)
},
getAutoUploadVideo () {
return window.djiBridge.mediaGetAutoUploadVideo()
},
setDownloadOwner (rcIndex:number):string {
return window.djiBridge.mediaSetDownloadOwner(rcIndex)
},
getDownloadOwner () {
return window.djiBridge.mediaGetDownloadOwner()
},
onBackClickReg () {
window.djiBridge.onBackClick = () => {
if (root.$router.currentRoute.value.path === '/pilot-home') {
console.log(root.$router.currentRoute.value.path)
return false
} else {
console.log(root.$router.currentRoute.value.path)
history.go(-1)
return true
}
}
}
}

9
src/api/wayline.ts

@ -0,0 +1,9 @@
import request from '/@/api/http/request'
const HTTP_PREFIX = '/wayline/api/v1'
// Get Wayline Files
export const getWaylineFiles = async function (wid: string, body: {}): Promise<any> {
const url = `${HTTP_PREFIX}/workspaces/${wid}/waylines?` + 'order_by=' + body.order_by + '&page=' + body.page + '&page_size=' + body.page_size
const result = await request.get(url)
return result.data
}

30
src/api/websocket.ts

@ -0,0 +1,30 @@
import ReconnectingWebSocket from 'reconnecting-websocket'
import { CURRENT_CONFIG as config } from '/@/api/http/config'
let socket = {}
export default {
init (getMsgFunc) {
const token = localStorage.getItem('x-auth-token')
const wspath =
config.websocketURL + '?x-auth-token=' + escape(token)
socket = new ReconnectingWebSocket(wspath)
socket.onopen = this.onOpen
socket.onerror = this.onError
socket.onmessage = getMsgFunc
socket.onclose = this.onClose
return socket
},
onOpen () {
console.log('ws opened')
},
onError (err) {
console.error(err)
},
onClose () {
console.log('ws closed')
},
sendMsg (data) {
this.socket.send(data)
}
}

11
src/assets/icons/check.svg

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>ic/panel/checkbox_active</title>
<desc>Created with Sketch.</desc>
<g id="ic/panel/checkbox_active" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-3" fill="#FFFFFF">
<polygon id="Shape-Copy" points="11.2103387 4.23529412 6.47425817 8.97137463 4.50089127 6.99800776 2.82352941 8.65563591 4.79689632 10.6290028 6.37558979 12.2076964 6.47425817 12.286631 12.8877005 5.8929223"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 731 B

16
src/assets/icons/dji-logo-vector.svg

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="layer" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 652 652" style="enable-background:new 0 0 652 652;" xml:space="preserve">
<style type="text/css">
.st0{fill:#241F1F;}
</style>
<g>
<path class="st0" d="M464.9,404.7l44.3-183.9h-95l-40.5,164.9c-5.9,32.2-40.4,47.2-64.9,47.6h-67.3l-22.9,66.3h141.5
C395,499.6,446.5,481.8,464.9,404.7"/>
<path class="st0" d="M265.5,339.9L310.1,153h97.8l-50.8,212.6c-9.8,41.1-40.3,50.9-68.5,50.9H63.2c-24.8,0-45.6-10.6-34.4-58
l20.3-84.8c10.3-43,42.3-52.9,65.4-52.9h157.3l-12.7,53h-80.3c-11.8,0-18.3,2.6-21.6,16.3l-13,54.1c-4.6,19.4,2.2,20.8,16.4,20.8
h73.6C247.8,365.2,259.6,364.3,265.5,339.9"/>
<polygon class="st0" points="530.7,220.9 484.6,416.5 579.7,416.5 625.7,220.9 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 952 B

4
src/assets/icons/layer.svg

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs><path d="m14.042 0 14.043 8.055-5.128 2.941 5.128 2.941-5.127 2.94 5.127 2.942-14.043 8.055L0 19.82l5.126-2.942L0 13.937l5.127-2.941L0 8.056 14.042 0zM7.638 18.318l-2.614 1.5 9.018 5.175 9.017-5.174-2.614-1.5-6.403 3.674-6.404-3.675zm0-5.882-2.615 1.5 9.02 5.175 9.017-5.173-2.615-1.501-6.403 3.674-6.404-3.675zm6.404-9.554L5.024 8.056l9.018 5.174 9.017-5.174-9.017-5.174z" id="layer_a"></path></defs><g transform="translate(2 2)" fill-rule="evenodd"><mask id="layer_b"><use xlink:href="#layer_a"></use></mask><use fill-rule="nonzero" xlink:href="#layer_a"></use><g mask="url(#layer_b)"><path d="M-19-20h68v68h-68z"></path></g></g>
</svg>

After

Width:  |  Height:  |  Size: 830 B

4
src/assets/icons/media.svg

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs><path d="M29.5 4v21.5h-4v2H3V8h3V4h23.5zM10.553 18.859 5.5 22.399V25H23v-6.088l-7.011 5.108-5.436-5.161zM27 6.5H8.499L8.5 8h17v15H27V6.5zm-4 4H5.5v8.847l5.293-3.707 5.406 5.133L23 15.819V10.5zm-6.362 1.956a2 2 0 1 1 0 4 2 2 0 0 1 0-4z" id="media_a"></path></defs><g fill-rule="evenodd"><mask id="media_b"><use xlink:href="#media_a"></use></mask><use fill-rule="nonzero" xlink:href="#media_a"></use><g mask="url(#media_b)"><path d="M-17-18h68v68h-68z"></path></g></g>
</svg>

After

Width:  |  Height:  |  Size: 666 B

22
src/assets/icons/pin-19be6b.svg

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>2图标/24px/pin</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
</defs>
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#19BE6B" fill-rule="evenodd">
<g id="编组" transform="translate(5.000000, 2.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="形状" fill="#19BE6B" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#19BE6B">
<g transform="translate(-26.000000, -26.000000)">
<rect x="0" y="0" width="68" height="68"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

22
src/assets/icons/pin-212121.svg

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>2图标/24px/pin</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
</defs>
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#212121" fill-rule="evenodd">
<g id="编组" transform="translate(5.000000, 2.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="形状" fill="#212121" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#212121">
<g transform="translate(-26.000000, -26.000000)">
<rect x="0" y="0" width="68" height="68"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

22
src/assets/icons/pin-2d8cf0.svg

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>2图标/24px/pin</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
</defs>
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#2D8CF0" fill-rule="evenodd">
<g id="编组" transform="translate(5.000000, 2.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="形状" fill="#2D8CF0" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#2D8CF0">
<g transform="translate(-26.000000, -26.000000)">
<rect x="0" y="0" width="68" height="68"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

22
src/assets/icons/pin-b620e0.svg

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>2图标/24px/pin</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
</defs>
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#b620e0" fill-rule="evenodd">
<g id="编组" transform="translate(5.000000, 2.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="形状" fill="#b620e0" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#b620e0">
<g transform="translate(-26.000000, -26.000000)">
<rect x="0" y="0" width="68" height="68"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

22
src/assets/icons/pin-e23c39.svg

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>2图标/24px/pin</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
</defs>
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#e23c39" fill-rule="evenodd">
<g id="编组" transform="translate(5.000000, 2.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="形状" fill="#e23c39" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#e23c39">
<g transform="translate(-26.000000, -26.000000)">
<rect x="0" y="0" width="68" height="68"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

22
src/assets/icons/pin-ffbb00.svg

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61.2 (89653) - https://sketch.com -->
<title>2图标/24px/pin</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M6.56239716,0 L13.1247943,9.89949494 L6.56239716,19.7989899 L0,9.89949494 L6.56239716,0 Z M6.56239716,3.61897251 L2.39965953,9.89878783 L6.56239716,16.1786032 L10.7251348,9.89878783 L6.56239716,3.61897251 Z" id="path-1"></path>
</defs>
<g id="2图标//24px/pin" stroke="none" stroke-width="1" fill="#FFBB00" fill-rule="evenodd">
<g id="编组" transform="translate(5.000000, 2.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="形状" fill="#FFBB00" fill-rule="nonzero" xlink:href="#path-1"></use>
<g id="1颜色/ic色/nor" mask="url(#mask-2)" fill="#FFBB00">
<g transform="translate(-26.000000, -26.000000)">
<rect x="0" y="0" width="68" height="68"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

14
src/assets/icons/tsa.svg

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path d="m19.87 10.131-1.01 2.518-.86-.012v-.006l-7.045.002-.068.004a1.75 1.75 0 0 0-1.614 1.737l.006.144.656 8.388h4.804l-1.005 2.5h-6.11l-.168-2.137H.544l-.53-6.896a4.25 4.25 0 0 1 4.03-4.598l.206-.005h2.48a4.25 4.25 0 0 1 .873.09 4.238 4.238 0 0 1 3.089-1.716l.166-.01.166-.003h8.846zm1.106 1.7 5.476 13.566-5.454-1.781-5.497 1.78 5.475-13.565zm.008 4.028.011 6.18H21l2.854.931-2.87-7.111zm-14.21-1.588L4.25 14.27a1.75 1.75 0 0 0-1.75 1.75l.007.153.352 4.597 4.401-.001-.473-6.056a4.3 4.3 0 0 1-.012-.442zM6.193 3.748a3.25 3.25 0 1 1 0 6.5 3.25 3.25 0 0 1 0-6.5zM14.3 0a4.25 4.25 0 1 1 0 8.5 4.25 4.25 0 0 1 0-8.5zM6.192 5.748a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5zM14.3 2a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5z" id="team_a">
</path>
</defs>
<g transform="translate(3 3)" fill-rule="evenodd">
<mask id="team_b">
<use xlink:href="#team_a"></use>
</mask>
<use fill-rule="nonzero" xlink:href="#team_a"></use>
<g mask="url(#team_b)"><path d="M-20-21h68v68h-68z"></path>
</g></g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

311
src/components/GMap.vue

@ -0,0 +1,311 @@
<template>
<div class="g-map-wrapper">
<div id="g-container" :style="{ width: '100%', height: '100%' }" />
<div
class="g-action-panle"
:style="{ right: drawVisible ? '316px' : '16px' }"
>
<div class="g-action-item" @click="draw('pin', true)">
<a-button type="primary">PIN</a-button>
</div>
<div class="g-action-item" @click="draw('polyline', true)">
<a-button type="primary">Line</a-button>
</div>
<div class="g-action-item" @click="draw('polygon', true)">
<a-button type="primary">Poly</a-button>
</div>
<div v-if="mouseMode" class="g-action-item" @click="draw('off', false)">
<a-button type="primary" danger>X</a-button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, reactive, ref, watch } from 'vue'
import {
generateLineContent,
generatePointContent,
generatePolyContent
} from '../utils/map-layer-utils'
import { postElementsReq } from '/@/api/layer'
import { MapDoodleType, MapElementEnum } from '/@/constants/map'
import { useGMapManage } from '/@/hooks/use-g-map'
import { useGMapCover } from '/@/hooks/use-g-map-cover'
import { useMouseTool } from '/@/hooks/use-mouse-tool'
import { getApp } from '/@/root'
import { useMyStore } from '/@/store'
import { GeojsonCoordinate } from '/@/types/map'
import { MapDoodleEnum } from '/@/types/map-enum'
import { PostElementsBody } from '/@/types/mapLayer'
import { uuidv4 } from '/@/utils/uuid'
import { gcj02towgs84, wgs84togcj02 } from '/@/vendors/coordtransform'
export default defineComponent({
name: 'GMap',
props: {},
setup () {
const useMouseToolHook = useMouseTool()
const useGMapManageHook = useGMapManage()
const mouseMode = ref(false)
const store = useMyStore()
const state = reactive({
currentType: '',
coverIndex: 0
})
const shareId = computed(() => {
return store.state.layerBaseInfo.share
})
const defaultId = computed(() => {
return store.state.layerBaseInfo.default
})
const drawVisible = computed(() => {
return store.state.drawVisible
})
watch(
() => store.state.wsEvent,
newData => {
const useGMapCoverHook = useGMapCover()
const event = newData
let exist = false
if (Object.keys(event.mapElementCreat).length !== 0) {
console.log(event.mapElementCreat)
const ele = event.mapElementCreat
store.state.Layers.forEach(layer => {
layer.elements.forEach(e => {
if (e.id === ele.id) {
exist = true
console.log('true')
}
})
})
if (exist === false) {
setLayers({
id: ele.id,
name: ele.name,
resource: ele.resource
})
updateCoordinates('wgs84-gcj02', ele)
useGMapCoverHook.init2DPin(
ele.name,
ele.resource.content.geometry.coordinates,
ele.resource.content.properties.color,
{
id: ele.id,
name: ele.name
}
)
}
store.state.wsEvent.mapElementCreat = {}
}
if (Object.keys(event.mapElementUpdate).length !== 0) {
console.log(event.mapElementUpdate)
console.log('该功能还未实现,请开发商自己增加')
store.state.wsEvent.mapElementUpdate = {}
}
if (Object.keys(event.mapElementDelete).length !== 0) {
console.log(event.mapElementDelete)
console.log('该功能还未实现,请开发商自己增加')
store.state.wsEvent.mapElementDelete = {}
}
},
{
deep: true
}
)
function draw (type: MapDoodleType, bool: boolean) {
state.currentType = type
useMouseToolHook.mouseTool(type, getDrawCallback)
mouseMode.value = bool
}
onMounted(() => {
const app = getApp()
useGMapManageHook.globalPropertiesConfig(app)
})
function getDrawCallback ({ obj }) {
switch (state.currentType) {
case MapDoodleEnum.PIN:
postPinPositionResource(obj)
break
case MapDoodleEnum.POLYLINE:
postPolylineResource(obj)
break
case MapDoodleEnum.POLYGON:
postPolygonResource(obj)
break
default:
break
}
}
async function postPinPositionResource (obj) {
const req = getPinPositionResource(obj)
setLayers(req)
updateCoordinates('gcj02-wgs84', req)
const result = await postElementsReq(shareId.value, req)
obj.setExtData({ id: req.id, name: req.name })
store.state.coverList.push(obj)
// console.log(store.state.coverList)
}
async function postPolylineResource (obj) {
const req = getPolylineResource(obj)
setLayers(req)
updateCoordinates('gcj02-wgs84', req)
const result = await postElementsReq(shareId.value, req)
obj.setExtData({ id: req.id, name: req.name })
store.state.coverList.push(obj)
// console.log(store.state.coverList)
}
async function postPolygonResource (obj) {
const req = getPoygonResource(obj)
setLayers(req)
updateCoordinates('gcj02-wgs84', req)
const result = await postElementsReq(shareId.value, req)
obj.setExtData({ id: req.id, name: req.name })
store.state.coverList.push(obj)
// console.log(store.state.coverList)
}
function getPinPositionResource (obj) {
const position = obj.getPosition()
const resource = generatePointContent(position)
const name = obj._originOpts.title
const id = uuidv4()
return {
id,
name,
resource
}
}
function getPolylineResource (obj) {
const path = obj.getPath()
const resource = generateLineContent(path)
const { name, id } = getBaseInfo(obj._opts)
return {
id,
name,
resource
}
}
function getPoygonResource (obj) {
const path = obj.getPath()
const resource = generatePolyContent(path)
const { name, id } = getBaseInfo(obj._opts)
return {
id,
name,
resource
}
}
function getBaseInfo (obj) {
const name = obj.title
const id = uuidv4()
return { name, id }
}
function setLayers (resource: PostElementsBody) {
const layers = store.state.Layers
const layer = layers.find(item => item.id.includes(shareId.value))
// layer.id = 'private_layer' + uuidv4()
// layer?.elements.push(resource)
if (layer?.elements) {
;(layer?.elements as any[]).push(resource)
}
console.log('layers', layers)
store.commit('SET_LAYER_INFO', layers)
}
function updateCoordinates (transformType: string, element: any) {
const geoType = element.resource?.content.geometry.type
const type = element.resource?.type as number
if (element.resource) {
if (MapElementEnum.PIN === type) {
const coordinates = element.resource?.content.geometry
.coordinates as GeojsonCoordinate
if (transformType === 'wgs84-gcj02') {
const transResult = wgs84togcj02(
coordinates[0],
coordinates[1]
) as GeojsonCoordinate
element.resource.content.geometry.coordinates = transResult
} else if (transformType === 'gcj02-wgs84') {
const transResult = gcj02towgs84(
coordinates[0],
coordinates[1]
) as GeojsonCoordinate
element.resource.content.geometry.coordinates = transResult
}
} else if (MapElementEnum.LINE === type && geoType === 'LineString') {
const coordinates = element.resource?.content.geometry
.coordinates as GeojsonCoordinate[]
if (transformType === 'wgs84-gcj02') {
coordinates.forEach(coordinate => {
coordinate = wgs84togcj02(
coordinate[0],
coordinate[1]
) as GeojsonCoordinate
})
} else if (transformType === 'gcj02-wgs84') {
coordinates.forEach(coordinate => {
coordinate = gcj02towgs84(
coordinate[0],
coordinate[1]
) as GeojsonCoordinate
})
}
element.resource.content.geometry.coordinates = coordinates
} else if (MapElementEnum.LINE === type && geoType === 'Polygon') {
const coordinates = element.resource?.content.geometry
.coordinates[0] as GeojsonCoordinate[]
if (transformType === 'wgs84-gcj02') {
coordinates.forEach(coordinate => {
coordinate = wgs84togcj02(
coordinate[0],
coordinate[1]
) as GeojsonCoordinate
})
} else if (transformType === 'gcj02-wgs84') {
coordinates.forEach(coordinate => {
coordinate = gcj02towgs84(
coordinate[0],
coordinate[1]
) as GeojsonCoordinate
})
}
element.resource.content.geometry.coordinates = [coordinates]
}
}
}
return {
draw,
mouseMode,
drawVisible
}
}
})
</script>
<style lang="scss" scoped>
.g-map-wrapper {
height: 100%;
width: 100%;
.g-action-panle {
position: absolute;
top: 16px;
right: 16px;
.g-action-item {
padding-top: 8px;
}
}
}
</style>
<style lang="scss">
.amap-logo {
opacity: 0;
}
.amap-copyright {
opacity: 0;
}
</style>

217
src/components/LayersTree.vue

@ -0,0 +1,217 @@
<template>
<span>
<a-tree
draggable
:defaultExpandAll="true"
class="device-map-layers"
@drop="onDrop"
v-bind="$attrs"
>
<a-tree-node
:title="layer.name"
:id="layer.id"
v-for="layer in getTreeData"
:key="layer.id"
>
<!-- <template #title>
{{layer.name}}
</template> -->
<template v-if="layer.elements">
<a-tree-node
v-for="resource in layer.elements"
:id="getLayerTreeKey('resource', resource.id)"
:key="getLayerTreeKey('resource', resource.id)"
>
<template #title>
{{ resource.name }}
</template>
</a-tree-node>
</template>
</a-tree-node>
</a-tree>
</span>
</template>
<script lang="ts" setup>
import { computed, defineProps, PropType, reactive } from 'vue'
import { useMyStore } from '/@/store'
import { DropEvent, mapLayer } from '/@/types/mapLayer'
import { getLayerTreeKey } from '/@/utils/layer-tree'
const store = useMyStore()
const props = defineProps({
layerData: Array as PropType<mapLayer[]>
})
const state = reactive({
checkedKeys: [] as string[],
expandedKeys: [] as string[]
})
const getTreeData = computed(() => {
// console.log('props.treeData', JSON.parse(JSON.stringify(props.layerData)))
return JSON.parse(JSON.stringify(props.layerData))
})
const shareId = computed(() => {
return store.state.layerBaseInfo.share
})
const defaultId = computed(() => {
return store.state.layerBaseInfo.default
})
async function onDrop ({ node, dragNode, dropPosition, dropToGap }: DropEvent) {
let _treeData = props.layerData || []
let dragKey = dragNode.eventKey
dragKey = dragKey.replaceAll('resource__', '')
const dropPos = node.pos.split('-')
let dropKey =
node.eventKey.includes(shareId.value) ||
node.eventKey.includes(defaultId.value)
? node.eventKey
: node.$parent.eventKey
if (!dragKey || !dropKey) return
dropKey = dropKey.replaceAll('resource__', '')
const loop = (data: mapLayer[], key: string, callback: Function) => {
data.forEach((item, index, arr) => {
if (item.id === key) {
return callback(item, index, arr)
}
if (item.elements) {
return loop(item.elements, key, callback)
}
})
}
const data = [..._treeData] as mapLayer[]
// Find dragObject
let dragObj = {} as mapLayer
loop(data, dragKey, (item: mapLayer, index: number, arr: mapLayer[]) => {
arr.splice(index, 1)
dragObj = item
})
if (!dropToGap) {
// Drop on the content
loop(data, dropKey, (item: mapLayer) => {
item.elements = item.elements || []
// where to insert
item.elements.push(dragObj)
})
}
_treeData = data
// console.log('_treeData', _treeData)
}
</script>
<style lang="scss">
$antPrefix: 'ant';
.device-map-layers.#{$antPrefix}-tree {
color: #fff;
.#{$antPrefix}-tree-checkbox:not(.#{$antPrefix}-tree-checkbox-checked)
.#{$antPrefix}-tree-checkbox-inner {
background-color: unset;
}
.anticon {
font-size: 16px;
}
// li 16px
> li {
padding-left: 16px;
padding-right: 16px;
}
li {
display: flex;
flex-wrap: wrap;
align-items: center;
padding-top: 0;
padding-bottom: 0;
&:first-child {
padding-top: 4px;
}
&.#{$antPrefix}-tree-treenode-disabled
> .#{$antPrefix}-tree-node-content-wrapper {
height: 20px;
span {
color: #fff;
}
}
> ul {
width: 100%;
}
.#{$antPrefix}-tree-switcher {
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
}
.#{$antPrefix}-tree-checkbox {
z-index: 1;
}
.#{$antPrefix}-tree-checkbox:hover::after,
.#{$antPrefix}-tree-checkbox-wrapper:hover
.#{$antPrefix}-tree-checkbox::after {
visibility: collapse;
}
.#{$antPrefix}-tree-title {
display: block;
}
.#{$antPrefix}-tree-node-content-wrapper {
color: #fff;
width: calc(100% - 46px);
flex: 1;
box-sizing: content-box;
height: 20px;
min-width: 0; //
padding-right: 0;
&:not([draggable='true']) {
border-top: 2px transparent solid;
border-bottom: 2px transparent solid;
}
&:hover {
background-color: transparent;
}
> span {
&::before {
// position: absolute;
// right: 0;
// left: 0;
height: 28px;
transition: all 0.3s;
content: '';
}
// positionrelative
> *:not(.progress-wrapper) {
position: relative;
z-index: 1;
}
}
&.#{$antPrefix}-tree-node-selected {
background-color: transparent;
color: #2d8cf0;
> span {
&::before {
background-color: #4f4f4f;
}
}
}
}
}
span.#{$antPrefix}-tree-switcher.#{$antPrefix}-tree-switcher_open
.#{$antPrefix}-tree-switcher-icon {
transform: rotate(0deg) !important;
}
span.#{$antPrefix}-tree-switcher.#{$antPrefix}-tree-switcher_close
.#{$antPrefix}-tree-switcher-icon {
transform: rotate(0deg) !important;
}
}
</style>

92
src/components/MediaPanel.vue

@ -0,0 +1,92 @@
<template>
<div class="media-panel-wrapper">
<div class="header">Media</div>
<a-button type="primary" style="margin-top:20px" @click="onRefresh"
>Refresh</a-button
>
<a-table class="media-table" :columns="columns" :data-source="data">
<template #name="{ text, record }">
<a :href="record.preview_url">{{ text }}</a>
</template>
<template #action>
<span class="action-area">
action
</span>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { ref } from '@vue/reactivity'
import { getMediaFiles } from '/@/api/media'
const columns = [
{
title: 'FileName',
dataIndex: 'name',
key: 'name',
slots: { customRender: 'name' }
},
{
title: 'FileSize',
dataIndex: 'size',
key: 'size'
},
{
title: 'PayloadType',
dataIndex: 'payload_type',
key: 'payload_type',
ellipsis: true
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
]
const data = ref([
{
key: '1',
name: 'name1',
size: 32,
payload_type: 'PM320_DUAL',
preview_url: ''
}
])
const onRefresh = async () => {
const wid = localStorage.getItem('workspace-id')
data.value = []
const index = 1
const res = await getMediaFiles(wid, {})
res.data.forEach(ele => {
data.value.push({
key: index.toString(),
name: ele.file_name
})
})
console.log(res)
}
</script>
<style lang="scss" scoped>
.media-panel-wrapper {
width: 100%;
.media-table {
background: #fff;
margin-top: 32px;
}
.header {
width: 100%;
height: 60px;
background: #fff;
padding: 16px 24px;
font-size: 20px;
text-align: start;
color: #000;
}
.action-area {
color: $primary;
cursor: pointer;
}
}
</style>

42
src/components/svgIcon.vue

@ -0,0 +1,42 @@
<template>
<svg :class="svgClass" :aria-hidden="true" :style="{color: color, width:computedWidth, height:computedWidth}">
<use :xlink:href="iconName" :fill="color"/>
</svg>
</template>
<script setup>
import { defineProps, computed } from 'vue'
const props = defineProps({
name: {
type: String,
required: true
},
color: {
type: String,
default: ''
},
size: {
type: Number,
},
})
const iconName = computed(() => `#icon-${props.name}`)
const svgClass = computed(() => {
console.log(props.name, 'props.name')
if (props.name) {
return `svg-icon icon-${props.name}`
}
return 'svg-icon'
})
const computedWidth = computed(() => {
const result = props.width || props.size
return result ? result + 'px' : '1em'
})
</script>
<style lang='scss'>
.svg-icon {
width: 1em;
height: 1em;
fill: currentColor;
vertical-align: middle;
}
</style>

122
src/components/wayline-panel.vue

@ -0,0 +1,122 @@
<template>
<div class="panel-wrapper">
<div class="header">Wayline Library</div>
<a-button type="primary" style="margin-top:20px" @click="onRefresh"
>Refresh</a-button
>
<a-table class="table" :columns="columns" :data-source="data">
<template #name="{ text, record }">
<a :href="record.preview_url">{{ text }}</a>
</template>
<template #action>
<span class="action-area">
action
</span>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { ref } from '@vue/reactivity'
import { onMounted } from 'vue'
import { getWaylineFiles } from '/@/api/wayline'
const columns = [
{
title: 'FileName',
dataIndex: 'name',
key: 'name',
slots: { customRender: 'name' }
},
{
title: 'TemplateType',
dataIndex: 'template_type',
key: 'template_type'
},
{
title: 'Favorited',
dataIndex: 'favorite',
key: 'favorite'
},
{
title: 'DroneType',
dataIndex: 'drone_type',
key: 'drone_type'
},
{
title: 'PayloadType',
dataIndex: 'payload_type',
key: 'payload_type'
},
{
title: 'User',
dataIndex: 'user',
key: 'user'
},
{
title: 'Action',
key: 'action',
slots: { customRender: 'action' }
}
]
const data = ref([
{
key: '1',
name: 'name1',
template_type: '0',
drone_type: '0-60-0',
payload_type: 'PM320_DUAL',
user: 'pilot',
favorited: 'true'
}
])
onMounted(() => {
onRefresh()
})
const onRefresh = async () => {
const wid: string = localStorage.getItem('workspace-id')
data.value = []
const index = 1
const res = await getWaylineFiles(wid, {
page: 1, //
page_size: 9, //
order_by: 'update_time desc' // , xxx_column_desc, xxx_column_asc, xxx_column(default asc)
})
console.log(res)
res.data.list.forEach(ele => {
data.value.push({
key: index.toString(),
name: ele.name,
template_type: ele.template_types[0],
drone_type: ele.drone_model_key,
payload_type: ele.payload_model_keys[0],
user: ele.user_name,
favorite: ele.favorited.toString()
})
})
console.log('wayline files:', data.value)
}
</script>
<style lang="scss" scoped>
.panel-wrapper {
width: 100%;
.table {
background: #fff;
margin-top: 32px;
}
.header {
width: 100%;
height: 60px;
background: #fff;
padding: 16px 24px;
font-size: 20px;
text-align: start;
color: #000;
}
.action-area {
color: $primary;
cursor: pointer;
}
}
</style>

19
src/constants/index.ts

@ -0,0 +1,19 @@
export const AMapConfig = {
key: '26d54da6733de88435c68d1a2e88b682',
version: '2.0',
plugins: [
'AMap.Scale',
'AMap.ToolBar',
'AMap.ControlBar',
'AMap.ElasticMarker',
'AMap.MapType',
'AMap.Geocoder',
'AMap.CircleEditor',
'AMap.PolygonEditor',
'AMap.PolylineEditor',
'AMap.PolyEditor',
'AMap.RangingTool',
'AMap.Weather',
'AMap.MouseTool'
]
}

23
src/constants/map.ts

@ -0,0 +1,23 @@
export enum MapElementColor {
Blue = '#2D8CF0',
Green = '#19BE6B',
Yellow = '#FFBB00',
Red = '#E23C39',
Orange = '#B620E0',
Default = '#212121'
}
export const MapElementDefaultColor = MapElementColor.Default
export enum MapDoodleColor {
PinColor = '#2D8CF0',
PolylineColor = '#3366FF',
PolygonColor = '#FF33FF'
}
export enum MapElementEnum {
PIN = 0,
LINE = 1,
POLY = 2
}
export type MapDoodleType = 'pin' | 'polyline' | 'polygon' | 'off'

138
src/constants/mock-layers.ts

@ -0,0 +1,138 @@
export const mapLayers = {
code: 0,
message: 'success',
data: {
list: [{
id: 'private_layer',
name: 'Private Layer',
order: 0,
is_distributed: false,
type: 1,
is_lock: false,
create_time: 1634268707424,
elements: [{
id: 'b2370d29-be65-42b0-9224-4d816e86dc64',
name: 'xuejia n. 1',
order: 0,
status: 1,
display: 1,
resource: {
content: {
type: 'Feature',
properties: {
color: '#2D8CF0'
},
geometry: {
type: 'Polygon',
coordinates: [
[
[114.156671, 38.468249],
[114.139517, 37.372177],
[115.52899, 37.712212]
]
]
}
},
type: 4,
user_name: 'xuejia n.',
user_id: '1402914943455727616'
},
update_time: 1636966336566,
create_time: 1636966325700,
elevation_load_status: 0,
icon: 'area'
}, {
id: '768e9fcd-121f-47a6-96b9-f1aee27d32f0',
name: 'xuejia n. 1',
order: 0,
status: 1,
display: 1,
resource: {
content: {
type: 'Feature',
properties: {
color: '#2D8CF0'
},
geometry: {
type: 'LineString',
coordinates: [
[116.263962, 40.234929],
[116.503006, 40.237026],
[116.335465, 40.155206],
[116.541458, 40.12371]
]
}
},
type: 4,
user_name: 'xuejia n.',
user_id: '1402914943455727616'
},
update_time: 1636966322636,
create_time: 1636966316803,
elevation_load_status: 0,
icon: 'line'
}, {
id: '4e741a76-3600-4af5-ace8-d805e7cd31fa',
name: 'xuejia n. 2',
order: 0,
status: 1,
display: 1,
resource: {
content: {
type: 'Feature',
properties: {
color: '#2D8CF0',
clampToGround: true
},
geometry: {
type: 'Point',
coordinates: [116.098223, 39.976538, 104]
}
},
type: 6,
user_name: 'xuejia n.',
user_id: '1402914943455727616'
},
update_time: 1636966305229,
create_time: 1636966305229,
elevation_load_status: 0,
icon: 'pin'
}, {
id: 'efff2b5d-de22-4d48-8d92-4f53170668f6',
name: 'xuejia n. 1',
order: 0,
status: 1,
display: 1,
resource: {
content: {
type: 'Feature',
properties: {
color: '#19BE6B',
clampToGround: true
},
geometry: {
type: 'Point',
coordinates: [113.35367028239645, 23.755194000519843, 22]
}
},
type: 6,
user_name: 'xuejia n.',
user_id: '1402914943455727616'
},
update_time: 1636966304432,
create_time: 1636966299455,
elevation_load_status: 0,
icon: 'pin'
}]
}, {
id: 'share_layer',
name: 'Share Layer',
order: 0,
is_distributed: true,
type: 2,
is_lock: false,
create_time: 1634268707414,
elements: []
}]
}
}

9
src/env.d.ts vendored

@ -0,0 +1,9 @@
// Environment variable definition
// https://cn.vitejs.dev/guide/env-and-mode.html#env-files
interface ImportMetaEnv {
VITE_APP_ENVIRONMENT: 'DEV' | 'STAG' | 'UAT' | 'PROD',
// api gateway
VITE_APP_APIGATEWAY_BACKEND_HOST: string
// More environment variables...
}

144
src/hooks/use-g-map-cover.ts

@ -0,0 +1,144 @@
import pin19be6b from '/@/assets/icons/pin-19be6b.svg'
import pin212121 from '/@/assets/icons/pin-212121.svg'
import pin2d8cf0 from '/@/assets/icons/pin-2d8cf0.svg'
import pinb620e0 from '/@/assets/icons/pin-b620e0.svg'
import pine23c39 from '/@/assets/icons/pin-e23c39.svg'
import pineffbb00 from '/@/assets/icons/pin-ffbb00.svg'
import { getRoot } from '/@/root'
import rootStore from '/@/store'
import { GeojsonCoordinate } from '/@/types/map'
export function useGMapCover () {
const root = getRoot()
const AMap = root.$aMapObj
const normalColor = '#2D8CF0'
const store = rootStore
const coverList = store.state.coverList
function AddCoverToMap (cover :any) {
root.$aMap.add(cover)
coverList.push(cover)
// console.log('coverList:', store.state.coverList)
}
function getPinIcon (color?:string) {
// console.log('color', color)
const colorObj: {
[key: number| string]: any
} = {
'2d8cf0': pin2d8cf0,
'19be6b': pin19be6b,
212121: pin212121,
b620e0: pinb620e0,
e23c39: pine23c39,
ffbb00: pineffbb00,
}
const iconName = (color?.replaceAll('#', '') || '').toLocaleLowerCase()
return new AMap.Icon({
// size: new AMap.Size(40, 50),
image: colorObj[iconName],
// imageOffset: new AMap.Pixel(0, -60),
// imageSize: new AMap.Size(40, 50)
})
}
function init2DPin (name: string, coordinates:GeojsonCoordinate, color?:string, data?:{}) {
console.log(name, coordinates[0], coordinates[1], color, data)
const pin = new AMap.Marker({
position: new AMap.LngLat(coordinates[0], coordinates[1]),
title: name,
icon: getPinIcon(color),
// strokeColor: color || normalColor,
// fillColor: color || normalColor,
extData: data
})
// console.log('coordinates pin', pin)
AddCoverToMap(pin)
}
function AddOverlayGroup (overlayGroup) {
root.$aMap.add(overlayGroup)
coverList.push(overlayGroup)
}
function initPolyline (name: string, coordinates:GeojsonCoordinate[], color?:string, data?:{}) {
const path = [] as GeojsonCoordinate[]
coordinates.forEach(coordinate => {
path.push(new AMap.LngLat(coordinate[0], coordinate[1]))
})
const polyline = new AMap.Polyline({
path: path,
strokeColor: color || normalColor,
strokeOpacity: 1,
strokeWeight: 2,
strokeStyle: 'solid',
extData: data
// draggable: true,
})
AddOverlayGroup(polyline)
}
function initPolygon (name: string, coordinates:GeojsonCoordinate[], color?:string, data?:{}) {
const path = [] as GeojsonCoordinate[]
coordinates.forEach(coordinate => {
path.push(new AMap.LngLat(coordinate[0], coordinate[1]))
})
// console.log('Polygon', path)
const Polygon = new AMap.Polygon({
path: path,
strokeOpacity: 1,
strokeWeight: 2,
fillColor: color || normalColor,
fillOpacity: 0.4,
// draggable: true,
strokeColor: color || normalColor,
extData: data
})
AddOverlayGroup(Polygon)
}
function removeCoverFromMap (id:string) {
for (let i = 0; i < coverList.length; i++) {
const ele = coverList[i]
// console.log(ele)
const extdata = ele?.getExtData()
if (extdata?.id === id) {
console.log(extdata)
root.$aMap.remove(ele)
coverList.slice(i, 1)
break
}
}
}
function getElementFromMap (id:string) {
// console.log('start', new Date().getTime())
const ele = coverList.find(ele => ele?.getExtData().id === id)
// console.log('end', new Date().getTime())
return ele
// coverList.forEach((ele:any) => {
// const extdata = ele?.getExtData()
// // console.log(extdata)
// if (extdata?.id === id) {
// return ele
// }
// })
}
function updatePinElement (id:string, name: string, coordinates:GeojsonCoordinate, color?:string) {
const element = getElementFromMap(id) as any
if (element) {
const icon = getPinIcon(color)
element.setPosition(new AMap.LngLat(coordinates[0], coordinates[1]))
element.setIcon(icon)
element.setTitle(name)
} else {
// console.log('into init PIN')
init2DPin(name, coordinates, color, {
id: id,
name: name
})
}
}
return {
init2DPin,
initPolyline,
initPolygon,
removeCoverFromMap,
getElementFromMap,
updatePinElement
}
}

34
src/hooks/use-g-map.ts

@ -0,0 +1,34 @@
import AMapLoader from '@amap/amap-jsapi-loader'
import { App, reactive } from 'vue'
import { AMapConfig } from '/@/constants/index'
export function useGMapManage () {
const state = reactive({
mapEntity: null,
mapObj: null,
mouseTool: null,
})
async function initMap (container: string, app:App) {
AMapLoader.load({
...AMapConfig
}).then((AMap) => {
state.mapObj = AMap
state.mapEntity = new AMap.Map(container, {
center: [113.935913, 22.525335],
zoom: 15
})
state.mouseTool = new AMap.MouseTool(state.mapEntity)
app.config.globalProperties.$aMap = state.mapEntity
app.config.globalProperties.$aMapObj = state.mapObj
app.config.globalProperties.$mouseTool = state.mouseTool
}).catch(e => {
console.log(e)
})
}
function globalPropertiesConfig (app:App) {
initMap('g-container', app)
}
return {
globalPropertiesConfig,
}
}

71
src/hooks/use-mouse-tool.ts

@ -0,0 +1,71 @@
import { reactive } from 'vue'
import pin2d8cf0 from '/@/assets/icons/pin-2d8cf0.svg'
import { MapDoodleType } from '/@/constants/map'
import { getRoot } from '/@/root'
import { MapDoodleEnum } from '/@/types/map-enum'
export function useMouseTool () {
const root = getRoot()
const AMap = root.$aMapObj
const state = reactive({
pinNum: 0,
polylineNum: 0,
PolygonNum: 0,
currentType: '',
})
function drawPin (type:MapDoodleType, getDrawCallback:Function) {
root?.$mouseTool.marker({
title: type + state.pinNum,
icon: pin2d8cf0,
})
state.pinNum++
root?.$mouseTool.on('draw', getDrawCallback)
}
function drawPolyline (type:MapDoodleType, getDrawCallback:Function) {
root?.$mouseTool.polyline({
strokeColor: '#2d8cf0',
strokeOpacity: 1,
strokeWeight: 2,
strokeStyle: 'solid',
draggable: true,
title: type + state.polylineNum++
})
root?.$mouseTool.on('draw', getDrawCallback)
}
function drawPolygon (type:MapDoodleType, getDrawCallback:Function) {
root?.$mouseTool.polygon({
strokeColor: '#2d8cf0',
strokeOpacity: 1,
strokeWeight: 2,
fillColor: '#1791fc',
fillOpacity: 0.4,
draggable: true,
title: type + state.PolygonNum++
})
root?.$mouseTool.on('draw', getDrawCallback)
}
function drawOff (type:MapDoodleType) {
root?.$mouseTool.close()
root?.$mouseTool.off('draw')
}
function mouseTool (type: MapDoodleType, getDrawCallback: Function) {
state.currentType = type
switch (type) {
case MapDoodleEnum.PIN:
drawPin(type, getDrawCallback)
break
case MapDoodleEnum.POLYLINE:
drawPolyline(type, getDrawCallback)
break
case MapDoodleEnum.POLYGON:
drawPolygon(type, getDrawCallback)
break
case MapDoodleEnum.Close:
drawOff(type)
break
}
}
return {
mouseTool
}
}

14
src/main.ts

@ -0,0 +1,14 @@
import App from './App.vue'
import router from './router'
import { antComponents } from './antd'
import { CommonComponents } from './use-common-components'
import 'virtual:svg-icons-register'
import store, { storeKey } from './store'
import { createInstance } from '/@/root'
import '/@/styles/index.scss'
const app = createInstance(App)
app.use(store, storeKey)
app.use(router)
app.use(CommonComponents)
app.use(antComponents)
app.mount('#demo-app')

21
src/pages/elements/elements.vue

@ -0,0 +1,21 @@
<template>
<div class="element-map-wrapper">
<GMap/>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted } from 'vue'
import GMap from '/@/components/GMap.vue'
export default defineComponent({
name: 'Elements',
components: { GMap },
setup () {
return {
}
},
})
</script>
<style lang="scss" scoped>
</style>

150
src/pages/page-pilot/pilot-home.vue

@ -0,0 +1,150 @@
<template>
<div class="page">
<div class="left flex-column flex-justify-start flex-align-center">
<p class="fz26 mb0 mt10" style="color: #727272">
{{ platformName }}
</p>
<p class="fz16 ml10 mb0 mt10" style="color: #2d8cf0">
status:{{ connect }}
</p>
<p class="fz32 mb0 mt50" style="color: #000000">{{ workspaceName }}</p>
<a-button
class="fz20 mt20 flex-column flex-justify-center flex-align-center"
style="width: 30vw; height: 12vh;"
type="default"
@click="onOpen3rdApp"
>Open 3rd Party APP</a-button
>
<a-button
class="fz20"
style="width: 15vw; height: 12vh; position: fixed; bottom: 7vh"
type="primary"
@click="onExit"
>Quit</a-button
>
</div>
<div class="right flex-column flex-justify-start flex-align-center">
<p class="fz24 mb0 mt10 ">Setting</p>
<a-button class="mt10 fz16" style="width:90%" @click="onMediaSetting"
>Media File Upload Setting</a-button
>
<a-button class="mt10 fz16" style="width:90%" @click="onLiveshareSetting"
>Manual Live Share Setting</a-button
>
</div>
</div>
</template>
<script lang="ts" setup>
import { message } from 'ant-design-vue'
import { onMounted, ref } from 'vue'
import { CURRENT_CONFIG } from '/@/api/http/config'
import { getPlatformInfo, getUserInfo } from '/@/api/manage'
import apiPilot from '/@/api/pilot-bridge'
import { getRoot } from '/@/root'
const root = getRoot()
const connect = ref('Disconnect')
const platformName = ref('Unknown')
const workspaceName = ref('Unknown')
const workspaceDesc = ref('Unknown')
const wsId = ref()
onMounted(() => {
apiPilot.init()
const token = apiPilot.getToken()
if (token) {
getPlatformInfo({}).then(res => {
console.log(res)
platformName.value = res.data.platform_name
workspaceName.value = res.data.workspace_name
workspaceDesc.value = res.data.workspace_desc
wsId.value = res.data.workspace_id
apiPilot.setPlatformMessage(
platformName.value,
workspaceName.value,
workspaceDesc.value
)
apiPilot.setWorkspaceId(wsId.value)
})
}
if (JSON.parse(apiPilot.isComponentLoaded('thing')).data === false || token) {
getUserInfo({}).then(res => {
const param = {
host: res.data.mqtt_addr,
username: res.data.mqtt_username,
password: res.data.mqtt_password,
connectCallback: 'connectCallback'
}
apiPilot.setComponentParam('thing', param)
apiPilot.loadComponent('thing', apiPilot.getComponentParam('thing'))
})
} else {
const connectState = JSON.parse(window.djiBridge.thingGetConnectState())
if (connectState.code === 0 && connectState.data) {
connect.value = 'Connected'
} else {
connect.value = 'Disconnect'
}
}
apiPilot.loadComponent('liveshare', apiPilot.getComponentParam('liveshare'))
console.log('ws token:', token)
apiPilot.setComponentParam('ws', {
host: CURRENT_CONFIG.websocketURL,
token: token
})
apiPilot.loadComponent('ws', apiPilot.getComponentParam('ws'))
apiPilot.setComponentParam('map', {
userName: 'pilot1',
elementPreName: 'PILOT'
})
apiPilot.loadComponent('map', apiPilot.getComponentParam('map'))
apiPilot.loadComponent('tsa', apiPilot.getComponentParam('tsa'))
apiPilot.loadComponent('media', apiPilot.getComponentParam('media'))
apiPilot.loadComponent('mission', {})
window.connectCallback = arg => {
connectCallback(arg)
}
apiPilot.onBackClickReg()
})
const connectCallback = (arg: any) => {
console.info('into callback', arg)
if (arg) {
connect.value = 'Connected'
window.djiBridge.mediaSetDownloadOwner(0)
window.djiBridge.mediaSetUploadPhotoType(1)
} else {
connect.value = 'Disconnect'
}
}
const onExit = async (e: any) => {
apiPilot.stopwebview()
}
const onMediaSetting = async (e: any) => {
root.$router.push('/pilot-media')
}
const onLiveshareSetting = async (e: any) => {
root.$router.push('/pilot-liveshare')
}
const onOpen3rdApp = () => {
window.open('mydjischeme://www.dji.com')
}
</script>
<style lang="scss" scoped>
@import '/@/styles/index.scss';
.page {
display: flex;
position: absolute;
transition: width 0.2s ease;
height: 100%;
width: 100%;
.left {
width: 50%;
border-right: red solid 2px;
}
.right {
width: 100%;
height: 100%;
}
}
</style>

145
src/pages/page-pilot/pilot-index.vue

@ -0,0 +1,145 @@
<template>
<div class="login flex-column flex-justify-center flex-align-center m0 b0">
<a-image
style="width: 17vw; height: 10vw; margin-bottom: 50px"
src="http://lofrev.net/wp-content/photos/2016/09/dji_logo_png.png"
/>
<p class="logo fz35 pb50">Pilot Cloud API Demo</p>
<a-form
layout="inline"
:model="formState"
class="flex-row flex-justify-center flex-align-center"
>
<a-form-item>
<a-input v-model:value="formState.user" placeholder="Username">
<template #prefix
><UserOutlined style="color: rgba(0, 0, 0, 0.25)"
/></template>
</a-input>
</a-form-item>
<a-form-item>
<a-input
v-model:value="formState.password"
type="password"
placeholder="Password"
>
<template #prefix
><LockOutlined style="color: rgba(0, 0, 0, 0.25)"
/></template>
</a-input>
</a-form-item>
<a-form-item>
<a-button
class="m0"
type="primary"
html-type="submit"
:disabled="formState.user === '' || formState.password === ''"
@click="onSubmit"
>
Login
</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { LockOutlined, UserOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { onMounted, reactive, UnwrapRef } from 'vue'
import { CURRENT_CONFIG } from '/@/api/http/config'
import { login, refreshToken } from '/@/api/manage'
import apiPilot from '/@/api/pilot-bridge'
import { getRoot } from '/@/root'
interface FormState {
user: string
password: string
}
const root = getRoot()
const formState: UnwrapRef<FormState> = reactive({
user: 'pilot',
password: 'pilot123'
})
let isVerified:any
onMounted(async () => {
const verifyLicense = JSON.parse(apiPilot.platformVerifyLicense(CURRENT_CONFIG.appId,
CURRENT_CONFIG.appKey, CURRENT_CONFIG.appLicense))
const platformVerify = JSON.parse(apiPilot.isPlatformVerifySuccess())
isVerified = platformVerify.data
if (platformVerify.data === true) {
message.success('The license verification is successful.')
} else {
message.error('Filed to verify the license. message is ' + verifyLicense.data)
return
}
const token = apiPilot.getToken()
console.log('api token:', token)
apiPilot.setPlatformMessage('Cloud Api Platform', '', '')
if (token && token !== undefined) {
await refreshToken({})
.then(res => {
apiPilot.setComponentParam('api', {
host: CURRENT_CONFIG.baseURL,
token: res.data.access_token
})
const jsres = JSON.parse(
apiPilot.loadComponent('api', apiPilot.getComponentParam('api'))
)
console.log('load api module status:', jsres)
apiPilot.setToken(res.data.access_token)
localStorage.setItem('x-auth-token', res.data.access_token)
message.success('Login Success')
root.$router.push('/pilot-home')
})
.catch(err => {
console.error(err)
})
}
})
const onSubmit = async (e: any) => {
await login({
username: formState.user,
password: formState.password
})
.then(res => {
if (!isVerified) {
message.error('Please verify the license firstly.')
return
}
console.log('login res:', res)
if (res.code === 0) {
apiPilot.setComponentParam('api', {
host: CURRENT_CONFIG.baseURL,
token: res.data.access_token
})
const jsres = apiPilot.loadComponent(
'api',
apiPilot.getComponentParam('api')
)
console.log('load api module res:', jsres)
apiPilot.setToken(res.data.access_token)
localStorage.setItem('x-auth-token', res.data.access_token)
localStorage.setItem('workspace-id', res.data.workspace_id)
localStorage.setItem('username', res.data.username)
message.success('Login Success')
root.$router.push('/pilot-home')
}
})
.catch(err => {
console.error(err)
})
}
</script>
<style lang="scss" scoped>
@import '/@/styles/index.scss';
.login {
// background-color: $dark-highlight;
height: 100vh;
}
.logo {
color: $primary;
}
</style>

202
src/pages/page-pilot/pilot-liveshare.vue

@ -0,0 +1,202 @@
<template>
<div
class="width-100vw height-100vh flex-column flex-justify-start flex-align-start"
>
<div
class="mt20 flex-row flex-align-center flex-justify-between"
style="width: 100%"
>
<p class="ml10 mb0 fz16" style="color: black">
Select Video Publish Mode:
</p>
<a-select
style="width: 200px"
placeholder="Select Mode"
@select="onPublishModeSelect"
>
<a-select-option
v-for="item in publishModeList"
:key="item.label"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</div>
<a-divider dashed class="mt10 mb0"></a-divider>
<div
class="flex-row flex-align-center flex-justify-between mt10"
style="width: 100%"
>
<p class="ml10 mb0 fz16" style="color: black">Select Live Share Type:</p>
<a-select
style="width: 200px"
placeholder="Select Live Type"
@select="onLiveTypeSelect"
>
<a-select-option
v-for="item in liveTypeList"
:key="item.label"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</div>
<a-divider dashed class="mt10 mb0"></a-divider>
<div
class="flex-row flex-align-center flex-justify-center mt20"
style="width: 100%"
>
<p>Live Share State: {{ liveState }}</p>
</div>
<div
class="flex-row flex-align-center flex-justify-center mt20"
style="width: 100%"
>
<a-button type="primary" @click="onPlay">Play</a-button>
<a-button class="ml20" type="primary" @click="onGetConfig"
>Get Config</a-button
>
<a-button class="ml20" type="primary" @click="onGetStatus"
>Get Status</a-button
>
</div>
</div>
</template>
<script lang="ts" setup>
import { message } from 'ant-design-vue'
import { onMounted, ref } from 'vue'
import { CURRENT_CONFIG as config } from '/@/api/http/config'
import apiPilot from '/@/api/pilot-bridge'
import { getRoot } from '/@/root'
const root = getRoot()
const publishModeList = [
{
value: 'video-on-demand',
label: 'video-on-demand'
},
{
value: 'video-by-manual',
label: 'video-by-manual'
},
{
value: 'video-demand-aux-manual',
label: 'video-demand-aux-manual'
}
]
const liveTypeList = [
{
value: 1,
label: 'AGORA'
},
{
value: 2,
label: 'RTMP'
},
{
value: 3,
label: 'RTSP'
},
{
value: 4,
label: 'GB28181'
}
]
const agoraParam = {
uid: config.agoraAPPID,
token: config.agoraToken,
channelId: config.agoraChannel
}
const rtmpParam = {
url: config.rtmpURL + '12345'
}
const rtspParam = {
userName: 'dji-live-share',
password: '12345678',
port: '8554'
}
const gb28181Param = {
serverIp: '114.55.103.238',
serverPort: '5060',
serverId: '34020000002000000001',
agentId: '34020000001310000004',
password: '12345678',
agentPort: '7060',
agentChannel: '34020000001310000004'
}
const liveState = ref<string>('STOP')
const livetypeSelected = ref<number>(1)
const publishModeSelected = ref<string>('video-demand-aux-manual')
onMounted(() => {
const status: any = apiPilot.getLiveshareStatus()
console.log(status)
// liveState.value =
// status.status === 0
// ? 'Cannot connect to server'
// : status.status === 1
// ? 'Connect to server'
// : 'Playing'
// console.log(liveState.value)
})
const onLiveTypeSelect = (val: any) => {
livetypeSelected.value = val
message.info('set livetype:' + livetypeSelected.value, 5)
}
const onPublishModeSelect = (val: string) => {
publishModeSelected.value = val
message.info(
'set publish mode res:' +
apiPilot.setVideoPublishType(publishModeSelected.value),
5
)
}
const onPlay = () => {
switch (livetypeSelected.value) {
case 1: {
message.info('agoraParam:' + JSON.stringify(agoraParam))
apiPilot.setLiveshareConfig(1, JSON.stringify(agoraParam))
apiPilot.startLiveshare()
break
}
case 2: {
message.info('rtmpParam:' + JSON.stringify(rtmpParam))
apiPilot.setLiveshareConfig(2, JSON.stringify(rtmpParam))
message.info(apiPilot.startLiveshare())
break
}
case 3: {
message.info('rtspParam:' + JSON.stringify(rtspParam))
apiPilot.setLiveshareConfig(3, JSON.stringify(rtspParam))
apiPilot.startLiveshare()
break
}
case 4: {
message.info('gb28181Param:' + JSON.stringify(gb28181Param))
apiPilot.setLiveshareConfig(4, JSON.stringify(gb28181Param))
apiPilot.startLiveshare()
break
}
}
}
const onGetStatus = () => {
const status = apiPilot.getLiveshareStatus()
message.info(status, 5)
}
const onGetConfig = () => {
const status = apiPilot.getLiveshareConfig()
message.info(status, 5)
}
</script>
<style lang="scss" scoped>
// @import '/@/styles/index.scss';
</style>

130
src/pages/page-pilot/pilot-media.vue

@ -0,0 +1,130 @@
<template>
<div
class="width-100vw height-100vh flex-column flex-justify-start flex-align-start"
>
<p class="fz16 ml10 mt10 mb10 color-text-title color-font-bold">
If Enabled, Pilot will upload photos or videos to the server
automatically.
</p>
<div
class="flex-row flex-align-center flex-justify-between"
style="width: 100%"
>
<p class="ml10 mb0 fz16" style="color: black">Auto Upload Photos</p>
<a-switch
class="mt0 mb0"
v-model:checked="enablePhotoUpload"
@change="onPhotoUpload"
></a-switch>
</div>
<div
class="flex-row flex-align-center flex-justify-between"
style="width: 100%"
>
<a-radio-group
class="mt10 ml20"
v-if="enablePhotoUpload == true"
v-model:value="photoType"
defaultChecked="0"
@change="onPhototype"
>
<a-radio :value="0">Original Photo</a-radio>
<a-radio class="ml20" :value="1">Preview Photo</a-radio>
</a-radio-group>
</div>
<a-divider dashed class="mt10 mb0"></a-divider>
<div
class="flex-row flex-align-center flex-justify-between mt10"
style="width: 100%"
>
<p class="ml10 mb0 fz16" style="color: black">Auto Upload Video</p>
<a-switch
@change="onVideoUpload"
v-model:checked="enableVideoUpload"
></a-switch>
</div>
<a-divider dashed class="mt10 mb0"></a-divider>
<div
class="flex-row flex-align-center flex-justify-between mt20"
style="width: 100%"
>
<p class="ml10 mb0 fz16 color-font-bold" style="color: black">
Path for uploading media resources in dual-controller mode
</p>
<a-radio-group
class="mt0 mb0"
v-model:value="uploadPath"
button-style="solid"
@change="onUploadPath"
>
<a-radio-button :value="0">Mine</a-radio-button>
<a-radio-button :value="1">Another</a-radio-button>
</a-radio-group>
</div>
</div>
</template>
<script lang="ts" setup>
import { message } from 'ant-design-vue'
import { onMounted, ref } from 'vue'
import apiPilot from '/@/api/pilot-bridge'
import { getRoot } from '/@/root'
const root = getRoot()
const enablePhotoUpload = ref<boolean>(true)
const enableVideoUpload = ref<boolean>(false)
const photoType = ref<number>(1)
const uploadPath = ref<number>(0)
onMounted(() => {
message.info('After setting, please use the physical button of the remote control to return, otherwise the setting is invalid.')
enablePhotoUpload.value =
apiPilot.getAutoUploadPhoto() === undefined
? true
: apiPilot.getAutoUploadPhoto()
enableVideoUpload.value =
apiPilot.getAutoUploadVideo() === undefined
? false
: apiPilot.getAutoUploadVideo()
photoType.value =
apiPilot.getUploadPhotoType() === undefined
? 1
: apiPilot.getUploadPhotoType()
uploadPath.value =
apiPilot.getDownloadOwner() === undefined ? 0 : apiPilot.getDownloadOwner()
console.log(
enablePhotoUpload.value,
enableVideoUpload.value,
photoType.value,
uploadPath.value
)
apiPilot.setComponentParam('media', {
autoUploadPhoto: enablePhotoUpload.value,
autoUploadPhotoType: photoType.value,
autoUploadVideo: enableVideoUpload.value
})
})
const onPhotoUpload = () => {
apiPilot.setAutoUploadPhoto(enablePhotoUpload.value)
}
const onVideoUpload = () => {
console.log(enableVideoUpload.value)
apiPilot.setAutoUploadVideo(enableVideoUpload.value)
}
const onPhototype = () => {
console.log(photoType.value)
apiPilot.setUploadPhotoType(photoType.value)
}
const onUploadPath = (e: any) => {
apiPilot.setDownloadOwner(uploadPath.value)
}
</script>
<style lang="scss" scoped>
// @import '/@/styles/index.scss';
</style>

208
src/pages/project-app/index.vue

@ -0,0 +1,208 @@
<template>
<div
v-if="showLogin"
class="login flex-column flex-justify-center flex-align-center m0 b0"
>
<a-image
style="width: 17vw; height: 10vw; margin-bottom: 50px"
src="http://lofrev.net/wp-content/photos/2016/09/dji_logo_png.png"
/>
<p class="logo fz35 pb50">Cloud API Demo</p>
<a-form
layout="inline"
:model="formState"
class="flex-row flex-justify-center flex-align-center"
>
<a-form-item>
<a-input v-model:value="formState.user" placeholder="Username">
<template #prefix
><UserOutlined style="color: rgba(0, 0, 0, 0.25)"
/></template>
</a-input>
</a-form-item>
<a-form-item>
<a-input
v-model:value="formState.password"
type="password"
placeholder="Password"
>
<template #prefix
><LockOutlined style="color: rgba(0, 0, 0, 0.25)"
/></template>
</a-input>
</a-form-item>
<a-form-item>
<a-button
class="m0"
type="primary"
html-type="submit"
:disabled="formState.user === '' || formState.password === ''"
@click="onSubmit"
>
Login
</a-button>
</a-form-item>
</a-form>
</div>
<div v-else class="project-app-wrapper">
<div class="left">
<Sidebar />
<div class="main-content uranus-scrollbar dark">
<router-view />
</div>
</div>
<div class="right">
<div class="map-wrapper">
<GMap />
</div>
<div class="media-wrapper" v-if="getMediaRoute()">
<MediaPanel />
</div>
<div class="wayline-wrapper" v-if="getWaylineRoute()">
<WaylinePanel />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { LockOutlined, UserOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { reactive, ref, UnwrapRef } from 'vue'
import Sidebar from './sidebar.vue'
import { login } from '/@/api/manage'
import websocket from '/@/api/websocket'
import GMap from '/@/components/GMap.vue'
import MediaPanel from '/@/components/MediaPanel.vue'
import WaylinePanel from '/@/components/wayline-panel.vue'
import { useGMapCover } from '/@/hooks/use-g-map-cover'
import { getRoot } from '/@/root'
import { useMyStore } from '/@/store'
interface FormState {
user: string
password: string
}
const root = getRoot()
const showLogin = ref(true)
const store = useMyStore()
const formState: UnwrapRef<FormState> = reactive({
user: 'adminPC',
password: 'adminPC'
})
let socket = {} as any
const gMapCoverHook = useGMapCover()
const onSubmit = async (e: any) => {
const result = await login({
username: formState.user,
password: formState.password
})
if (result.code === 0) {
console.log(result)
localStorage.setItem('x-auth-token', result.data.access_token)
localStorage.setItem('workspace-id', result.data.workspace_id)
localStorage.setItem('username', result.data.username)
showLogin.value = false
message.info('login success')
socket = websocket.init(wsGetMsg)
}
}
// function wsInfo (data) {
// store.commit('SET_DEVICE_INFO', data)
// }
// function getDeviceInfo () {
// const info = store.state.DeviceInfo
// console.log(info)
const wsGetMsg = async (res: any) => {
const payload = JSON.parse(res.data)
// console.log(payload)
switch (payload.biz_code) {
case 'gateway_osd': {
store.commit('SET_GATEWAY_INFO', payload.data)
break
}
case 'device_osd': {
store.commit('SET_DEVICE_INFO', payload.data)
break
}
case 'map_element_create': {
store.commit('SET_MAP_ELEMENT_CREATE', payload.data)
break
}
case 'map_element_update': {
store.commit('SET_MAP_ELEMENT_UPDATE', payload.data)
break
}
case 'map_element_delete': {
store.commit('SET_MAP_ELEMENT_DELETE', payload.data)
break
}
default:
break
}
}
function getMediaRoute () {
return root.$route.name === 'media'
}
function getWaylineRoute () {
return root.$route.name === 'wayline'
}
</script>
<style lang="scss" scoped>
@import '/@/styles/index.scss';
.login {
background-color: $dark-highlight;
height: 100vh;
}
.logo {
color: $primary;
}
.project-app-wrapper {
display: flex;
position: absolute;
transition: width 0.2s ease;
height: 100%;
width: 100%;
.left {
width: 450px;
display: flex;
background-color: #232323;
float: left;
}
.right {
width: 100%;
height: 100%;
.map-wrapper {
width: 100%;
height: 100%;
}
}
.main-content {
flex: 1;
color: $text-white-basic;
}
.media-wrapper {
position: absolute;
top: 0;
bottom: 0;
z-index: 100;
background: #f6f8fa;
padding: 16px;
}
.wayline-wrapper {
position: absolute;
top: 0;
bottom: 0;
z-index: 100;
background: #f6f8fa;
padding: 16px;
}
}
</style>

438
src/pages/project-app/projects/layer.vue

@ -0,0 +1,438 @@
<template>
<div class="project-layer-wrapper">
<LayersTree
:layer-data="mapLayers"
class="project-layer-content"
@check="checkLayer"
@select="selectLayer"
v-model:selectedKeys="selectedKeys"
v-model:checkedKeys="checkedKeys"
/>
<a-drawer
title="Map Element"
placement="right"
:closable="true"
v-model:visible="visible"
:mask="false"
wrapClassName="drawer-element-wrapper"
@close="closeDrawer"
width="300"
>
<div class="drawer-element-content">
<div class="name element-item">
<span class="title">Name:</span>
<a-input
v-model:value="layerState.layerName"
style="width:120px"
placeholder="element name"
@change="changeLayer"
/>
</div>
<div
class="longitude element-item"
v-if="layerState.currentType === geoType.Point"
>
<span class="title">Longitude:</span>
<a-input
v-model:value="layerState.longitude"
style="width:120px"
placeholder="longitude"
@change="changeLayer"
/>
</div>
<div
class="latitude element-item"
v-if="layerState.currentType === geoType.Point"
>
<span class="title">Latitude:</span>
<a-input
v-model:value="layerState.latitude"
style="width:120px"
placeholder="latitude"
@change="changeLayer"
/>
</div>
<div class="color-content">
<span class="mr30">Color: </span>
<div
v-for="item in colors"
:key="item.id"
class="color-item"
:style="'background:' + item.color"
@click="changeColor(item)"
>
<svg-icon
v-if="item.color === layerState.color"
:size="18"
name="check"
></svg-icon>
</div>
</div>
</div>
<div class="flex-row flex-justify-around flex-align-center mt20">
<a-button type="primary" @click="deleteElement">Delete</a-button>
</div>
</a-drawer>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, watch } from 'vue'
import {
deleteElementReq,
getElementGroupsReq,
updateElementsReq
} from '/@/api/layer'
import LayersTree from '/@/components/LayersTree.vue'
import { MapDoodleColor, MapElementEnum } from '/@/constants/map'
import { useGMapCover } from '/@/hooks/use-g-map-cover'
import { getRoot } from '/@/root'
import { useMyStore } from '/@/store'
import { GeojsonCoordinate, LayerResource } from '/@/types/map'
import { Color, GeoType } from '/@/types/mapLayer'
import { generatePoint } from '/@/utils/genjson'
import { gcj02towgs84, wgs84togcj02 } from '/@/vendors/coordtransform'
const root = getRoot()
const store = useMyStore()
let useGMapCoverHook = useGMapCover(store)
console.log('store', store)
const mapLayers = ref(store.state.Layers)
const checkedKeys = ref<string[]>([])
const selectedKeys = ref<string[]>([])
const selectedKey = ref<string>('')
const selectedLayer = ref<any>(null)
const visible = ref<boolean>(false)
store.commit('SET_DRAW_VISIBLE_INFO', visible.value)
const geoType = GeoType
const layerState = reactive({
layerName: '',
layerId: '',
longitude: 0,
latitude: 0,
currentType: '', // LineString,"Polygon","Point"
color: '#212121'
})
const colors = ref<Color[]>([
{ id: 1, name: 'BLUE', color: '#2D8CF0', selected: true },
{ id: 2, name: 'GREEN', color: '#19BE6B', selected: false },
{ id: 3, name: 'YELLOW', color: '#FFBB00', selected: false },
{ id: 4, name: 'ORANGE', color: '#B620E0', selected: false },
{ id: 5, name: 'RED', color: '#E23C39', selected: false },
{ id: 6, name: 'NAME_DEFAULT', color: '#212121', selected: false }
])
async function getAllElement () {
getElementGroups('init')
setTimeout(() => {
useGMapCoverHook = useGMapCover()
initMapCover()
}, 1000)
}
function initMapCover () {
mapLayers.value.forEach(item => {
if (item.elements) {
setMapCoverByElement(item.elements)
}
})
}
watch(
() => store.state.Layers,
newData => {
mapLayers.value = newData
},
{
deep: true
}
)
function setMapCoverByElement (elements: LayerResource[]) {
elements.forEach(element => {
const name = element.name
const color = element.resource?.content.properties.color
const type = element.resource?.type as number
updateMapElement(element, name, color)
})
}
function updateMapElement (
element: LayerResource,
name: string,
color: string | undefined
) {
const geoType = element.resource?.content.geometry.type
const id = element.id
const type = element.resource?.type as number
if (MapElementEnum.PIN === type) {
const coordinates = element.resource?.content.geometry
.coordinates as GeojsonCoordinate
useGMapCoverHook.updatePinElement(id, name, coordinates, color)
} else if (MapElementEnum.LINE === type && geoType === 'LineString') {
const coordinates = element.resource?.content.geometry
.coordinates as GeojsonCoordinate[]
useGMapCoverHook.initPolyline(name, coordinates, color, {
id: id,
name: name
})
} else if (MapElementEnum.LINE === type && geoType === 'Polygon') {
const coordinates = element.resource?.content.geometry
.coordinates[0] as GeojsonCoordinate[]
useGMapCoverHook.initPolygon(name, coordinates, color, {
id: id,
name: name
})
}
}
function checkLayer (keys: string[]) {
console.log('checkLayer', keys, selectedKeys.value, checkedKeys.value)
}
function selectLayer (keys: string[], e) {
// console.log('selectLayer', e.node.eventKey, e.selected)
if (e.selected) {
selectedKey.value = e.node.eventKey
selectedLayer.value = getCurrentLayer(selectedKey.value)
setBaseInfo()
}
visible.value = e.selected
store.commit('SET_DRAW_VISIBLE_INFO', visible.value)
// store.dispatch('updateElement', { type: 'is_select', id: e.node.eventKey, bool: e.selected })
}
function getCurrentLayer (id: string) {
const Layers = store.state.Layers
const key = id.replaceAll('resource__', '')
// console.log('selectedKey.value', selectedKey.value)
let layer = null
const findCan = function (V) {
V.forEach(item => {
if (item.id === key) {
layer = item
}
if (item.elements) {
findCan(item.elements)
}
})
}
findCan(Layers)
// const layer = Layers.find(item => item.elements.find(el => el.id === key))
console.log('layer', layer)
return layer
}
function setBaseInfo () {
const layer = selectedLayer.value
if (layer) {
const geoType = layer.resource?.content.geometry.type
// LineString,"Polygon","Point"
layerState.currentType = geoType
layerState.layerName = layer.name
layerState.layerId = layer.id
layerState.color = layer.resource?.content.properties.color
switch (geoType) {
case GeoType.Point:
layerState.longitude = layer.resource?.content.geometry.coordinates[0]
layerState.latitude = layer.resource?.content.geometry.coordinates[1]
break
case GeoType.LineString:
break
case GeoType.Polygon:
break
}
}
}
onMounted(() => {
getAllElement()
})
function closeDrawer () {
store.commit('SET_DRAW_VISIBLE_INFO', false)
selectedKeys.value = []
}
function changeColor (color: Color) {
layerState.color = color.color
updateElements()
}
function changeLayer (val: string) {
updateElements()
}
async function deleteElement () {
const elementid = selectedLayer.value.id
await deleteElementReq(elementid, {}).then(async (res: any) => {
// console.log('delete element res:', res)
if (res.code !== 0) {
console.warn(res)
return
}
visible.value = false
store.commit('SET_DRAW_VISIBLE_INFO', visible.value)
useGMapCoverHook.removeCoverFromMap(elementid)
getElementGroups()
})
}
async function getElementGroups (type?: string) {
const result = await getElementGroupsReq({
groupId: '',
isDistributed: true
})
mapLayers.value = result.data
mapLayers.value = updateWgs84togcj02()
if (type && type === 'init') {
store.dispatch('setLayerInfo', mapLayers.value)
}
store.commit('SET_LAYER_INFO', mapLayers.value)
}
async function updateElements () {
let content = null
if (layerState.currentType === GeoType.Point) {
const position = {
height: 0,
latitude: Number(layerState.latitude || 0),
longitude: Number(layerState.longitude || 0)
}
const cxt = generatePoint(position, {
color: layerState.color || MapDoodleColor.PinColor,
clampToGround: true
})
content = {
type: MapElementEnum.PIN,
geometry: cxt.geometry,
properties: cxt.properties
}
const currentLayer = selectedLayer.value
currentLayer.resource.content = content
selectedLayer.value = currentLayer
} else {
const currentLayer = selectedLayer.value
content = currentLayer.resource.content
content.properties.color = layerState.color
}
updateMapElement(selectedLayer.value, layerState.layerName, layerState.color)
const result = await updateElementsReq(layerState.layerId, {
name: layerState.layerName,
content: content
})
getElementGroups()
}
function updateWgs84togcj02 () {
const layers = mapLayers.value
layers.forEach(item => {
if (item.elements) {
item.elements.forEach(ele => {
updateCoordinates('wgs84-gcj02', ele)
})
}
})
return layers
}
function updateCoordinates (transformType: string, element: LayerResource) {
const geoType = element.resource?.content.geometry.type
const type = element.resource?.type as number
if (element.resource) {
if (MapElementEnum.PIN === type) {
const coordinates = element.resource?.content.geometry
.coordinates as GeojsonCoordinate
if (transformType === 'wgs84-gcj02') {
const transResult = wgs84togcj02(
coordinates[0],
coordinates[1]
) as GeojsonCoordinate
element.resource.content.geometry.coordinates = transResult
} else if (transformType === 'gcj02-wgs84') {
const transResult = gcj02towgs84(
coordinates[0],
coordinates[1]
) as GeojsonCoordinate
element.resource.content.geometry.coordinates = transResult
}
} else if (MapElementEnum.LINE === type && geoType === 'LineString') {
const coordinates = element.resource?.content.geometry
.coordinates as GeojsonCoordinate[]
if (transformType === 'wgs84-gcj02') {
coordinates.forEach(coordinate => {
coordinate = wgs84togcj02(
coordinate[0],
coordinate[1]
) as GeojsonCoordinate
})
} else if (transformType === 'gcj02-wgs84') {
coordinates.forEach(coordinate => {
coordinate = gcj02towgs84(
coordinate[0],
coordinate[1]
) as GeojsonCoordinate
})
}
element.resource.content.geometry.coordinates = coordinates
} else if (MapElementEnum.LINE === type && geoType === 'Polygon') {
const coordinates = element.resource?.content.geometry
.coordinates[0] as GeojsonCoordinate[]
if (transformType === 'wgs84-gcj02') {
coordinates.forEach(coordinate => {
coordinate = wgs84togcj02(
coordinate[0],
coordinate[1]
) as GeojsonCoordinate
})
} else if (transformType === 'gcj02-wgs84') {
coordinates.forEach(coordinate => {
coordinate = gcj02towgs84(
coordinate[0],
coordinate[1]
) as GeojsonCoordinate
})
}
element.resource.content.geometry.coordinates = [coordinates]
}
}
}
</script>
<style lang="scss" scoped>
@import '/@/styles/index.scss';
.project-layer-wrapper {
padding-top: 16px;
}
</style>
<style lang="scss">
.drawer-element-wrapper {
.ant-drawer-content {
background-color: $dark-highlight;
color: $text-white-basic;
.ant-drawer-header {
background-color: $dark-highlight;
.ant-drawer-title {
color: $text-white-basic;
}
.ant-drawer-close {
color: $text-white-basic;
}
}
.ant-input {
background-color: #101010;
border-color: $dark-border;
color: $text-white-basic;
}
}
.color-content {
display: flex;
align-items: center;
margin-top: 8px;
.color-item {
cursor: pointer;
width: 18px;
height: 18px;
line-height: 18px;
display: flex;
align-items: center;
margin-left: 5px;
}
}
.title {
display: inline-flex;
width: 80px;
}
.element-item {
margin-bottom: 10px;
}
}
</style>

327
src/pages/project-app/projects/livestream-agora.vue

@ -0,0 +1,327 @@
<template>
<div class="flex-column flex-justify-start flex-align-center">
<p class="fz24">Live streaming source selection</p>
<div class="flex-row flex-justify-center flex-align-center mt10">
<a-select
style="width:150px"
placeholder="Select Drone"
@select="onDroneSelect"
>
<a-select-option
v-for="item in dronePara.droneList"
:key="item.value"
:value="item.value"
>{{ item.label }}</a-select-option
>
</a-select>
<a-select
class="ml10"
style="width:150px"
placeholder="Select Camera"
@select="onCameraSelect"
>
<a-select-option
v-for="item in dronePara.cameraList"
:key="item.value"
:value="item.value"
>{{ item.label }}</a-select-option
>
</a-select>
<a-select
class="ml10"
style="width:150px"
placeholder="Select Lens"
@select="onVideoSelect"
>
<a-select-option
class="ml10"
v-for="item in dronePara.videoList"
:key="item.value"
:value="item.value"
>{{ item.label }}</a-select-option
>
</a-select>
<a-select
class="ml10"
style="width:150px"
placeholder="Select Clarity"
@select="onClaritySelect"
>
<a-select-option
v-for="item in clarityList"
:key="item.value"
:value="item.value"
>{{ item.label }}</a-select-option
>
</a-select>
</div>
<p class="fz24 mt10">Agora Parameter</p>
<p class="fz16">
Note: Obtain The Following Parameters From https://console.agora.io
</p>
<div class="flex-row flex-justify-center flex-align-center">
<a-input v-model:value="agoraPara.appid" placeholder="APP ID"></a-input>
<a-input
class="ml10"
v-model:value="agoraPara.token"
placeholder="Token"
></a-input>
<a-input
class="ml10"
v-model:value="agoraPara.channel"
placeholder="Channel"
></a-input>
</div>
<div class="mt20">
<p class="fz20">
Livestate:{{ livePara.liveState == false ? 'Stop' : 'Playing' }}
</p>
</div>
<div class="mt10 flex-row flex-justify-center flex-align-center">
<a-button type="primary" large @click="onStart">Play</a-button>
<a-button class="ml20" type="primary" large @click="onStop"
>Stop</a-button
>
<a-button class="ml20" type="primary" large @click="onRefresh"
>Refresh Live Capacity</a-button
>
</div>
<div id="player"></div>
</div>
</template>
<script lang="ts" setup>
import AgoraRTC from 'agora-rtc-sdk-ng'
import { onMounted, reactive } from 'vue'
import { CURRENT_CONFIG as config } from '/@/api/http/config'
import { getLiveCapacity, startLivestream, stopLivestream } from '/@/api/manage'
import { getRoot } from '/@/root'
const root = getRoot()
const clarityList = [
{
value: 0,
label: 'Adaptive'
},
{
value: 1,
label: 'Smooth'
},
{
value: 2,
label: 'Standard'
},
{
value: 3,
label: 'HD'
},
{
value: 4,
label: 'Super Clear'
}
]
let agoraClient = {} as any
const agoraPara = reactive({
appid: config.agoraAPPID,
token: config.agoraToken,
channel: config.agoraChannel,
uid: null,
stream: {}
})
const dronePara = reactive({
livestreamSource: [],
droneList: [],
cameraList: [],
videoList: [],
droneSelected: '',
cameraSelected: '',
videoSelected: '',
claritySelected: ''
})
const livePara = reactive({
url: '',
webrtc: {} as any,
videoId: '',
liveState: false
})
const onRefresh = async () => {
await getLiveCapacity({})
.then(res => {
console.log(res)
if (res.code === 0) {
if (res.data === null) {
console.warn('warning: get live capacity is null!!!')
return
}
dronePara.livestreamSource = res.data
dronePara.droneList = []
console.log('live_capacity:', dronePara.livestreamSource)
if (dronePara.livestreamSource) {
dronePara.livestreamSource.forEach((ele: any) => {
dronePara.droneList.push({ label: ele.sn, value: ele.sn })
})
console.log(dronePara.droneList)
}
}
})
.catch(error => {
console.error(error)
})
}
onMounted(() => {
onRefresh()
agoraClient = AgoraRTC.createClient({ mode: 'live', codec: 'h264' })
agoraClient.setClientRole('audience')
// Subscribe when a remote user publishes a stream
agoraClient.on('user-published', async (user: any, mediaType: string) => {
await agoraClient.subscribe(user, mediaType)
if (mediaType === 'video') {
console.log('subscribe success')
// Get `RemoteVideoTrack` in the `user` object.
const remoteVideoTrack = user.videoTrack
// Dynamically create a container in the form of a DIV element for playing the remote video track.
const remotePlayerContainer: any = document.getElementById('player')
// Specify the ID of the DIV container. You can use the `uid` of the remote user.
remotePlayerContainer.id = agoraPara.uid
remotePlayerContainer.textContent = 'uid: ' + agoraPara.uid
remotePlayerContainer.style.width = '640px'
remotePlayerContainer.style.height = '480px'
remoteVideoTrack.play(remotePlayerContainer)
}
})
agoraClient.on('user-unpublished', async (user: any) => {
console.log('unpublish live:', user)
await agoraClient.leave()
})
})
const handleError = (err: any) => {
console.error(err)
}
const handleJoinChannel = (uid: any) => {
agoraPara.uid = uid
}
const onStart = async () => {
const that = this
console.log(
'drone parameter:',
dronePara.droneSelected,
dronePara.cameraSelected,
dronePara.videoSelected,
dronePara.claritySelected
)
const timestamp = new Date().getTime().toString()
const liveTimestamp = timestamp
if (
dronePara.droneSelected == null ||
dronePara.cameraSelected == null ||
dronePara.videoSelected == null ||
dronePara.claritySelected == null
) {
console.warn('waring: not select live para!!!')
}
livePara.videoId =
dronePara.droneSelected +
'/' +
dronePara.cameraSelected +
'/' +
dronePara.videoSelected
console.log(agoraPara)
await agoraClient
.join(agoraPara.appid, agoraPara.channel, agoraPara.token, null)
.then((res: any) => {
console.log('agora uid:', res)
agoraPara.uid = res
})
console.log(agoraPara.token)
agoraPara.token = encodeURIComponent(agoraPara.token)
console.log('agoraToken:', agoraPara.token)
livePara.url =
'channel=' +
agoraPara.channel +
'&sn=' +
dronePara.droneSelected +
'&token=' +
agoraPara.token +
'&uid=' +
agoraPara.uid
await startLivestream({
url: livePara.url,
video_id: livePara.videoId,
url_type: 0,
video_quality: dronePara.claritySelected
})
.then(res => {
livePara.liveState = true
})
.catch(err => {
console.error(err)
})
}
const onStop = async () => {
stopLivestream({
video_id: livePara.videoId
}).then(res => {
livePara.liveState = false
console.log('stop play livestream')
})
}
const onDroneSelect = (val: any) => {
dronePara.droneSelected = val
if (dronePara.droneSelected) {
const droneTemp = dronePara.livestreamSource
droneTemp.forEach(ele => {
const drone = ele
if (drone.sn === dronePara.droneSelected) {
const cameraListTemp = drone.cameras_list
dronePara.cameraList = []
cameraListTemp.forEach((ele: any) => {
dronePara.cameraList.push({ label: ele.name, value: ele.index })
})
}
})
}
}
const onCameraSelect = (val: any) => {
dronePara.cameraSelected = val
if (dronePara.cameraSelected) {
const droneTemp = dronePara.livestreamSource
droneTemp.forEach(ele => {
const drone = ele
if (drone.sn === dronePara.droneSelected) {
const cameraListTemp = drone.cameras_list
cameraListTemp.forEach((ele: any) => {
const camera = ele
if (camera.index === dronePara.cameraSelected) {
const videoListTemp = camera.videos_list
dronePara.videoList = []
videoListTemp.forEach((ele: any) => {
dronePara.videoList.push({ label: ele.type, value: ele.index })
})
}
})
}
})
}
}
const onVideoSelect = (val: any) => {
dronePara.videoSelected = val
}
const onClaritySelect = (val: any) => {
dronePara.claritySelected = val
}
</script>
<style lang="scss" scoped>
@import '/@/styles/index.scss';
</style>

351
src/pages/project-app/projects/livestream-others.vue

@ -0,0 +1,351 @@
<template>
<div class="flex-column flex-justify-start flex-align-center">
<video
:style="{ width: '720px', height: '480px' }"
id="video-webrtc"
ref="videowebrtc"
controls
class="mt20"
></video>
<p class="fz24">Live streaming source selection</p>
<div class="flex-row flex-justify-center flex-align-center mt10">
<a-select
style="width: 150px"
placeholder="Select Live Type"
@select="onLiveTypeSelect"
>
<a-select-option
v-for="item in liveTypeList"
:key="item.label"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
<a-select
class="ml10"
style="width:150px"
placeholder="Select Drone"
@select="onDroneSelect"
>
<a-select-option
v-for="item in droneList"
:key="item.value"
:value="item.value"
>{{ item.label }}</a-select-option
>
</a-select>
<a-select
class="ml10"
style="width:150px"
placeholder="Select Camera"
@select="onCameraSelect"
>
<a-select-option
v-for="item in cameraList"
:key="item.value"
:value="item.value"
>{{ item.label }}</a-select-option
>
</a-select>
<a-select
class="ml10"
style="width:150px"
placeholder="Select Lens"
@select="onVideoSelect"
>
<a-select-option
class="ml10"
v-for="item in videoList"
:key="item.value"
:value="item.value"
>{{ item.label }}</a-select-option
>
</a-select>
<a-select
class="ml10"
style="width:150px"
placeholder="Select Clarity"
@select="onClaritySelect"
>
<a-select-option
v-for="item in clarityList"
:key="item.value"
:value="item.value"
>{{ item.label }}</a-select-option
>
</a-select>
</div>
<div class="mt20">
<p class="fz20">Livestate:{{ liveState == 0 ? 'Stop' : 'Playing' }}</p>
<p class="fz10" v-if="livetypeSelected == 2">
Please use VLC media player to play the RTSP livestream !!!
</p>
<p class="fz10" v-if="livetypeSelected == 2">
RTSP Parameter:{{ rtspData }}
</p>
</div>
<div class="mt10 flex-row flex-justify-center flex-align-center">
<a-button type="primary" large @click="onStart">Play</a-button>
<a-button class="ml20" type="primary" large @click="onStop"
>Stop</a-button
>
<a-button class="ml20" type="primary" large @click="onRefresh"
>Refresh Live Capacity</a-button
>
</div>
</div>
</template>
<script lang="ts" setup>
import { message } from 'ant-design-vue'
import { onMounted, ref } from 'vue'
import { CURRENT_CONFIG as config } from '/@/api/http/config'
import { getLiveCapacity, startLivestream, stopLivestream } from '/@/api/manage'
import { getRoot } from '/@/root'
import jswebrtc from '/@/vendors/jswebrtc.min.js'
const root = getRoot()
const liveTypeList = [
{
value: 1,
label: 'RTMP'
},
{
value: 2,
label: 'RTSP'
},
{
value: 3,
label: 'GB28181'
}
]
const clarityList = [
{
value: 0,
label: 'Adaptive'
},
{
value: 1,
label: 'Smooth'
},
{
value: 2,
label: 'Standard'
},
{
value: 3,
label: 'HD'
},
{
value: 4,
label: 'Super Clear'
}
]
const videowebrtc = ref(null)
const livestreamSource = ref()
const droneList = ref()
const cameraList = ref()
const videoList = ref()
const droneSelected = ref()
const cameraSelected = ref()
const videoSeleted = ref()
const claritySeleted = ref()
const videoId = ref()
const liveState = ref(0)
const livetypeSelected = ref()
const rtspData = ref()
const onRefresh = async () => {
await getLiveCapacity({})
.then(res => {
console.log(res)
if (res.code === 0) {
if (res.data === null) {
console.warn('warning: get live capacity is null!!!')
return
}
const resData: Array<[]> = res.data
console.log('live_capacity:', resData)
livestreamSource.value = resData
console.log(livestreamSource)
const temp: Array<{}> = []
if (livestreamSource.value) {
livestreamSource.value.forEach(ele => {
temp.push({ label: ele.sn, value: ele.sn })
})
console.log(temp)
droneList.value = temp
console.log(droneList.value)
}
}
})
.catch(error => {
console.error(error)
})
}
onMounted(() => {
onRefresh()
})
const onStart = async () => {
const that = this
console.log(
'直播参数:',
livetypeSelected.value,
droneSelected.value,
cameraSelected.value,
videoSeleted.value,
claritySeleted.value
)
const timestamp = new Date().getTime().toString()
const liveTimestamp = timestamp
if (
livetypeSelected.value == null ||
droneSelected.value == null ||
cameraSelected.value == null ||
videoSeleted.value == null ||
claritySeleted.value == null
) {
console.warn('waring: not select live para!!!')
return
}
videoId.value =
droneSelected.value + '/' + cameraSelected.value + '/' + videoSeleted.value
let liveURL = ''
switch (livetypeSelected.value) {
case 1: {
// RTMP
liveURL = config.rtmpURL + timestamp
break
}
case 2: {
// RTSP
liveURL = config.rtspPara
break
}
case 3: {
// GB28181
liveURL = config.gb28181Para
break
}
default:
console.warn('warning: live type is not correct!!!')
break
}
await startLivestream({
url: liveURL,
video_id: videoId.value,
url_type: livetypeSelected.value,
video_quality: claritySeleted.value
})
.then(res => {
if (livetypeSelected.value === 3) {
const url = res.data.url
const videoElement = videowebrtc.value
// gb28181,it will fail if not wait.
message.loading({
content: '直播等待中。。。',
duration: 4,
onClose () {
const player = new jswebrtc.Player(url, {
video: videoElement,
autoplay: true,
onPlay: obj => {
console.log('start play livestream')
}
})
liveState.value = 1
}
})
} else if (livetypeSelected.value === 2) {
console.log(res)
rtspData.value =
'url:' +
res.data.url +
'&username:' +
res.data.username +
'&password:' +
res.data.password
} else if (livetypeSelected.value === 1) {
const url = res.data.url
const videoElement = videowebrtc.value
console.log('start live:', url)
const player = new jswebrtc.Player(url, {
video: videoElement,
autoplay: true,
onPlay: obj => {
console.log('start play livestream')
liveState.value = 1
}
})
}
})
.catch(err => {
console.error(err)
})
}
const onStop = () => {
stopLivestream({
video_id: videoId.value
}).then(res => {
liveState.value = 0
console.log('stop play livestream')
})
}
const onLiveTypeSelect = (val: any) => {
livetypeSelected.value = val
}
const onDroneSelect = (val: any) => {
droneSelected.value = val
const temp: Array<{}> = []
if (droneSelected.value) {
const droneTemp = livestreamSource.value
droneTemp.forEach(ele => {
const drone = ele
if (drone.sn === droneSelected.value) {
const cameraListTemp = drone.cameras_list
cameraListTemp.forEach(ele => {
temp.push({ label: ele.name, value: ele.index })
})
cameraList.value = temp
}
})
}
}
const onCameraSelect = (val: any) => {
cameraSelected.value = val
const result: Array<{}> = []
if (cameraSelected.value) {
const droneTemp = livestreamSource.value
droneTemp.forEach(ele => {
const drone = ele
if (drone.sn === droneSelected.value) {
const cameraListTemp = drone.cameras_list
cameraListTemp.forEach(ele => {
const camera = ele
if (camera.index === cameraSelected.value) {
const videoListTemp = camera.videos_list
videoListTemp.forEach(ele => {
result.push({ label: ele.type, value: ele.index })
})
videoList.value = result
}
})
}
})
}
}
const onVideoSelect = (val: any) => {
videoSeleted.value = val
}
const onClaritySelect = (val: any) => {
claritySeleted.value = val
}
</script>
<style lang="scss" scoped>
@import '/@/styles/index.scss';
</style>

82
src/pages/project-app/projects/livestream.vue

@ -0,0 +1,82 @@
<template>
<div class="flex-column flex-justify-start flex-align-center">
<a-button
class="mt10 "
style="width:90%"
type="primary"
@click="onAgoraLiveStream"
>Agora Live</a-button
>
<a-button
class="mt10"
style="width:90%"
type="primary"
@click="onOthersLive"
>RTMP/GB28181 Live</a-button
>
</div>
<div v-if="enableAgoraLive">
<a-modal
style="top:0"
v-model:visible="enableAgoraLive"
title="Agora Live"
width="100%"
:maskClosable="false"
wrapClassName="full-modal"
:footer="null"
>
<LiveAgora />
</a-modal>
</div>
<div v-if="enableOthersLive">
<a-modal
style="top:0"
v-model:visible="enableOthersLive"
title="RTMP/GB28181/RTSP Live"
width="100%"
:maskClosable="false"
wrapClassName="full-modal"
:footer="null"
>
<LiveOthers />
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import LiveAgora from './livestream-agora.vue'
import LiveOthers from './livestream-others.vue'
import { getRoot } from '/@/root'
const root = getRoot()
const enableAgoraLive = ref(false)
const enableOthersLive = ref(false)
const onAgoraLiveStream = () => {
console.log('agora')
enableAgoraLive.value = true
}
const onOthersLive = () => {
console.log('liveview')
enableOthersLive.value = true
}
</script>
<style lang="scss">
.full-modal {
.ant-modal {
max-width: 100%;
top: 0;
padding-bottom: 0;
margin: 0;
}
.ant-modal-content {
display: flex;
flex-direction: column;
height: calc(100vh);
}
.ant-modal-body {
flex: 1;
}
}
</style>

11
src/pages/project-app/projects/media.vue

@ -0,0 +1,11 @@
<template>
<div class="project-media-wrapper">
Media
</div>
</template>
<script lang="ts" setup>
</script>
<style lang="scss" scoped>
</style>

11
src/pages/project-app/projects/tsa.vue

@ -0,0 +1,11 @@
<template>
<div class="project-tsa-wrapper">
TSA
</div>
</template>
<script lang="ts" setup>
</script>
<style lang="scss" scoped>
</style>

9
src/pages/project-app/projects/wayline.vue

@ -0,0 +1,9 @@
<template>
<div class="project-wayline-wrapper">
wayline
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped></style>

103
src/pages/project-app/sidebar.vue

@ -0,0 +1,103 @@
<template>
<div class="demo-project-sidebar-wrapper">
<router-link
v-for="item in options"
:key="item.key"
:to="item.path"
:class="{
'menu-item': true,
selected: selectedRoute(item),
disabled: item.key > 6
}"
>
<a-tooltip :title="item.label" placement="right">
<span>{{ item.label }}</span>
</a-tooltip>
</router-link>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { getRoot } from '/@/root'
interface IOptions {
key: number
label: string
path:
| string
| {
path: string
query?: any
}
icon: string
}
export default defineComponent({
name: 'Sidebar',
setup () {
const root = getRoot()
const options = [
{ key: 0, label: 'livestream', path: '/livestream', icon: 'livestream' },
{ key: 1, label: 'tsa', path: '/tsa', icon: 'tsa' },
{ key: 2, label: 'layer', path: '/layer', icon: 'layer' },
{ key: 3, label: 'media', path: '/media', icon: 'media' },
{ key: 4, label: 'wayline', path: '/wayline', icon: 'wayline' }
]
function selectedRoute (item: IOptions) {
const path = typeof item.path === 'string' ? item.path : item.path.path
return root.$route.path?.indexOf(path) === 0
}
return {
options,
selectedRoute
}
}
})
</script>
<style scoped lang="scss">
.demo-project-sidebar-wrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 80px;
border-right: 1px solid #4f4f4f;
color: $text-white-basic;
// flex: 1;
overflow: hidden;
.menu-item {
width: 100%;
padding: 16px 0px;
display: flex;
flex-direction: column;
align-items: center;
color: $text-white-basic;
cursor: pointer;
&.selected {
background-color: $dark-highlight;
color: $primary;
}
&.disabled {
pointer-events: none;
opacity: 0.45;
}
}
.filling {
flex: 1;
}
.setting-icon {
font-size: 24px;
margin-bottom: 24px;
color: $text-white-basic;
}
}
</style>
<style>
.ant-tooltip-open {
border: 0;
}
</style>

50
src/plugins/svgBuilder.ts

@ -0,0 +1,50 @@
import { readFileSync, readdirSync } from 'fs'
let idPerfix = ''
const svgTitle = /<svg([^>+].*?)>/
const clearHeightWidth = /(width|height)="([^>+].*?)"/g
const hasViewBox = /(viewBox="[^>+].*?")/g
const clearReturn = /(\r)|(\n)/g
// Find the svg file
function svgFind(e) {
const arr = []
const dirents = readdirSync(e, { withFileTypes: true })
for (const dirent of dirents) {
if (dirent.isDirectory()) arr.push(...svgFind(e + dirent.name + '/'))
else {
const svg = readFileSync(e + dirent.name)
.toString()
.replace(clearReturn, '')
.replace(svgTitle, ($1, $2) => {
let width = 0
let height = 0
let content = $2.replace(clearHeightWidth, (s1, s2, s3) => {
if (s2 === 'width') width = s3
else if (s2 === 'height') height = s3
return ''
})
if (!hasViewBox.test($2)) content += `viewBox="0 0 ${width} ${height}"`
return `<symbol id="${idPerfix}-${dirent.name.replace('.svg', '')}" ${content}>`
}).replace('</svg>', '</symbol>')
arr.push(svg)
}
}
return arr
}
export const svgBuilder = (path: any, perfix = 'icon') => {
if (path === '') return
idPerfix = perfix
const res = svgFind(path)
console.log(res)
return {
name: 'svg-transform',
transformIndexHtml (dom: String) {
return dom.replace(
'<body>',
`<body><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0" version="1.1">${res.join('')}</svg>`
)
}
}
}

24
src/root.ts

@ -0,0 +1,24 @@
import { createApp, ComponentCustomProperties, App as VueApp } from 'vue'
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$aMap: any
$aMapObj: any
$mouseTool: any
}
}
let root: ComponentCustomProperties
let app = null as any
export function createInstance (App: any): VueApp {
app = createApp(App)
root = app.config.globalProperties as ComponentCustomProperties
return app
}
export function getRoot (): ComponentCustomProperties {
return root
}
export function getApp (): VueApp {
return app
}

70
src/router/index.ts

@ -0,0 +1,70 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { ERouterName } from '/@/types/index'
const routes: Array<RouteRecordRaw> = [
{
path: '/' + ERouterName.Project,
name: ERouterName.Project,
// redirect: {
// name: ERouterName.Project
// },
component: () => import('/@/pages/project-app/index.vue'),
children: [
{
path: '/' + ERouterName.Livestream,
component: () => import('/@/pages/project-app/projects/livestream.vue')
},
{
path: '/' + ERouterName.Tsa,
component: () => import('/@/pages/project-app/projects/tsa.vue')
},
{
path: '/' + ERouterName.Layer,
name: ERouterName.Layer,
component: () => import('/@/pages/project-app/projects/layer.vue')
},
{
path: '/' + ERouterName.Media,
name: ERouterName.Media,
component: () => import('/@/pages/project-app/projects/media.vue')
},
{
path: '/' + ERouterName.Wayline,
name: ERouterName.Wayline,
component: () => import('/@/pages/project-app/projects/wayline.vue')
},
]
},
{
path: '/' + ERouterName.Pilot,
name: ERouterName.Pilot,
component: () => import('/@/pages/page-pilot/pilot-index.vue'),
children: [
]
},
{
path: '/' + ERouterName.PilotHome,
component: () => import('/@/pages/page-pilot/pilot-home.vue')
},
{
path: '/' + ERouterName.PilotMedia,
component: () => import('/@/pages/page-pilot/pilot-media.vue')
},
{
path: '/' + ERouterName.PilotLiveshare,
component: () => import('/@/pages/page-pilot/pilot-liveshare.vue')
},
{
path: '/' + ERouterName.Element,
name: ERouterName.Element,
component: () => import('/@/pages/elements/elements.vue')
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
export default router

5
src/shims-vue.d.ts vendored

@ -0,0 +1,5 @@
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

135
src/store/index.ts

@ -0,0 +1,135 @@
import { InjectionKey } from 'vue'
import { ActionTree, createStore, GetterTree, MutationTree, Store, StoreOptions, useStore } from 'vuex'
import { getLayers } from '/@/api/layer'
import { LayerType } from '/@/types/mapLayer'
const initStateFunc = () => ({
Layers: [
{
name: 'default',
id: '',
is_distributed: true,
elements: [],
is_check: false,
is_select: false,
type: 1
},
{
name: 'share',
id: '',
is_distributed: true,
elements: [],
is_check: false,
is_select: false,
type: 2
}
],
GatewayInfo: { // remote controller, dock
},
DeviceInfo: { // drone
},
layerBaseInfo: {} as {
[key:string]:string
},
drawVisible: false,
coverList: [
] as any,
wsEvent: {
mapElementCreat: {},
mapElementUpdate: {},
mapElementDelete: {}
}
})
export type RootStateType = ReturnType<typeof initStateFunc>
const getters: GetterTree<RootStateType, RootStateType> = {
}
const mutations: MutationTree<RootStateType> = {
SET_LAYER_INFO (state, info) {
state.Layers = info
},
SET_DEVICE_INFO (state, info) {
state.DeviceInfo = info
// console.log(state.DeviceInfo)
},
SET_GATEWAY_INFO (state, info) {
state.GatewayInfo = info
// console.log(state.GatewayInfo)
},
SET_DRAW_VISIBLE_INFO (state, bool) {
state.drawVisible = bool
},
SET_MAP_ELEMENT_CREATE (state, info) {
state.wsEvent.mapElementCreat = info
},
SET_MAP_ELEMENT_UPDATE (state, info) {
state.wsEvent.mapElementUpdate = info
},
SET_MAP_ELEMENT_DELETE (state, info) {
state.wsEvent.mapElementDelete = info
},
}
const actions: ActionTree<RootStateType, RootStateType> = {
async getAllElement ({ commit }) {
const result = await getLayers({
groupId: '',
isDistributed: true
})
commit('SET_LAYER_INFO', result.data?.list)
console.log(result)
},
updateElement ({ state }, content: {type: 'is_check' | 'is_select', id: string, bool:boolean}) {
const key = content.id.replaceAll('resource__', '')
const type = content.type
const layers = state.Layers
const layer = layers.find(item => item.id === key)
if (layer) {
layer[type] = content.bool
}
},
setLayerInfo ({ state }, layers) {
// const layers = state.Layers
const obj:{
[key:string]:string
} = {}
layers.forEach(layer => {
if (layer.type === LayerType.Default) {
obj.default = layer.id
} else {
if (layer.type === LayerType.Share) {
obj.share = layer.id
}
}
})
state.layerBaseInfo = obj
console.log('state.layerBaseInfo', state.layerBaseInfo)
},
getLayerInfo ({ state }, id:string) {
return state.layerBaseInfo[id]
}
}
const storeOptions: StoreOptions<RootStateType> = {
state: initStateFunc,
getters,
mutations,
actions
}
const rootStore = createStore(storeOptions)
export default rootStore
export const storeKey: InjectionKey<Store<RootStateType>> = Symbol('')
type AllStateStoreTypes = RootStateType & {
// moduleName: moduleType
}
export function useMyStore<T = AllStateStoreTypes> () {
return useStore<T>(storeKey)
}

19
src/styles/common.scss

@ -0,0 +1,19 @@
html, body, #app, #my-app {
height: 100%;
overflow: hidden;
}
body {
background-color: #f7f9fa;
-webkit-font-smoothing: antialiased;
// Prevent font enlargement in horizontal screen
text-size-adjust: 100%;
font-family: Roboto, sans-serif-medium, Arial, sans-serif;
color: $main-text-color;
font-size: 14px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

318
src/styles/flex.style.scss

@ -0,0 +1,318 @@
.flex-display {
display: flex;
}
.flex-column {
@extend .flex-display;
flex-direction: column;
}
.flex-row {
@extend .flex-display;
flex-direction: row;
}
.flex-align-start {
align-items: flex-start;
}
.flex-align-end {
align-items: flex-end;
}
.flex-align-baseline {
align-items: baseline;
}
.flex-align-stretch {
align-items: stretch;
}
.flex-align-center {
align-items: center;
}
.flex-justify-start {
justify-content: flex-start;
}
.flex-justify-end {
justify-content: flex-end;
}
.flex-justify-center {
justify-content: center;
}
.flex-justify-between {
justify-content: space-between;
}
.flex-justify-around {
justify-content: space-around;
}
//width
.width-100vw {
width: 100vw;
}
.width-100 {
width: 100%;
}
//height
.height-100vh {
height: 100vh;
}
.height-100 {
height: 100%;
}
//margin
m-5 {
margin: -5px !important;
}
.mt-5 {
margin-top: -5px !important;
}
.mt100 {
margin-top: 100px !important;
}
.mt110 {
margin-top: 110px !important;
}
.mb-5 {
margin-bottom: -5px !important;
}
.ml-5 {
margin-left: -5px !important;
}
.mr-5 {
margin-right: -5px !important;
}
.m0 {
margin: 0px !important;
}
.mt0 {
margin-top: 0px !important;
}
.mb0 {
margin-bottom: 0px !important;
}
.ml0 {
margin-left: 0px !important;
}
.mr0 {
margin-right: 0px !important;
}
.m5 {
margin: 5px !important;
}
.mt5 {
margin-top: 5px !important;
}
.mb5 {
margin-bottom: 5px !important;
}
.ml5 {
margin-left: 5px !important;
}
.mr5 {
margin-right: 5px !important;
}
.m10 {
margin: 10px !important;
}
.mt10 {
margin-top: 10px !important;
}
.mb10 {
margin-bottom: 10px !important;
}
.ml10 {
margin-left: 10px !important;
}
.mr10 {
margin-right: 10px !important;
}
.m15 {
margin: 15px !important;
}
.mt15 {
margin-top: 15px !important;
}
.mb15 {
margin-bottom: 15px !important;
}
.ml15 {
margin-left: 15px !important;
}
.mr15 {
margin-right: 15px !important;
}
.m20 {
margin: 20px !important;
}
.mt20 {
margin-top: 20px !important;
}
.mb20 {
margin-bottom: 20px !important;
}
.ml20 {
margin-left: 20px !important;
}
.mr20 {
margin-right: 20px !important;
}
.m25 {
margin: 25px !important;
}
.mt25 {
margin-top: 25px !important;
}
.mb25 {
margin-bottom: 25px !important;
}
.ml25 {
margin-left: 25px !important;
}
.mr25 {
margin-right: 25px !important;
}
.m30 {
margin: 30px !important;
}
.mt30 {
margin-top: 30px !important;
}
.mb30 {
margin-bottom: 30px !important;
}
.ml30 {
margin-left: 30px !important;
}
.ml40 {
margin-left: 40px !important;
}
.mr30 {
margin-right: 30px !important;
}
.m50 {
margin: 50px !important;
}
.mt50 {
margin-top: 50px !important;
}
.mb50 {
margin-bottom: 50px !important;
}
.ml50 {
margin-left: 50px !important;
}
.mr50 {
margin-right: 50px !important;
}
// padding值
.p0 {
padding: 0 !important;
}
.pt0 {
padding-top: 0 !important;
}
.pr0 {
padding-right: 0 !important;
}
.pb0 {
padding-bottom: 0 !important;
}
.pl0 {
padding-left: 0 !important;
}
.p5 {
padding: 5px;
}
.pt5 {
padding-top: 5px;
}
.pr5 {
padding-right: 5px;
}
.pb5 {
padding-bottom: 5px;
}
.pl5 {
padding-left: 5px;
}
.p10 {
padding: 10px;
}
.pt10 {
padding-top: 10px;
}
.pr10 {
padding-right: 10px;
}
.pb10 {
padding-bottom: 10px;
}
.pl10 {
padding-left: 10px;
}
.p15 {
padding: 15px;
}
.pt15 {
padding-top: 15px;
}
.pr15 {
padding-right: 15px;
}
.pb15 {
padding-bottom: 15px;
}
.pl15 {
padding-left: 15px;
}
.p20 {
padding: 20px;
box-sizing: border-box;
}
.pt20 {
padding-top: 20px;
}
.pr20 {
padding-right: 20px;
}
.pb20 {
padding-bottom: 20px;
}
.pl20 {
padding-left: 20px;
}
.p30 {
padding: 30px;
box-sizing: border-box;
}
.pt30 {
padding-top: 30px;
}
.pr30 {
padding-right: 30px;
}
.pb30 {
padding-bottom: 30px;
}
.pl30 {
padding-left: 30px;
}
.pb50 {
padding-bottom: 50px;
}
.pl50 {
padding-left: 50px;
}
.pl120 {
padding-left: 120px;
}
.pl150 {
padding-left: 150px;
}
.pt50 {
padding-top: 50px;
}

72
src/styles/fonts.scss

@ -0,0 +1,72 @@
$font-family-sans-serif: 'Open Sans', BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', SimSun, sans-serif;
$line-heights: (
12: 20px,
14: 22px,
16: 24px,
18: 26px
);
// 用法: @include text(12)
@mixin text($size) {
font-size: #{$size}px;
line-height: map-get($line-heights, $size);
}
// 常规体
@mixin text-regular {
font-weight: 400;
}
// 中粗体
@mixin text-semibold {
font-weight: 600;
}
@mixin ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fz10 {
font-size: 10;
}
.fz12 {
font-size: 12px;
}
.fz14 {
font-size: 14px;
}
.fz16 {
font-size: 16px;
}
.fz18 {
font-size: 18px;
}
.fz20 {
font-size: 20px;
}
.fz22 {
font-size: 22px;
}
.fz24 {
font-size: 24px;
}
.fz26 {
font-size: 26px;
}
.fz28 {
font-size: 28px;
}
.fz30 {
font-size: 30px;
}
.fz32 {
font-size: 32px;
}
.fz35 {
font-size: 35px;
}

3
src/styles/index.scss

@ -0,0 +1,3 @@
@import './common.scss';
@import 'flex.style.scss';
@import 'fonts.scss';

164
src/styles/reset.scss

@ -0,0 +1,164 @@
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin : 0;
padding : 0;
border : 0;
font : inherit;
font-size : 100%;
vertical-align: baseline;
}
html {
line-height : 1;
// -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
ol,
ul {
list-style: none;
}
table {
border-collapse: collapse;
border-spacing : 0;
}
caption,
th,
td {
font-weight : normal;
vertical-align: middle;
}
q,
blockquote {
quotes: none;
}
q::before,
q::after,
blockquote::before,
blockquote::after {
content: '';
content: none;
}
a img {
border: none;
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section,
summary {
display: block;
}
* {
box-sizing: content-box;
}
a {
background : transparent;
text-decoration: none;
}
button,
input[type='number'],
input[type='text'],
input[type='password'],
input[type='email'],
input[type='search'],
select,
textarea {
font-family : inherit;
margin : 0;
-webkit-appearance: none;
}

72
src/styles/variables.scss

@ -0,0 +1,72 @@
$main-text-color: #000;
$header-height: 52px;
// Auxiliary color
$info: #1fa3f6;
$success: #28d445;
$danger: #e70102;
$alarm: #ffcc00;
$warning: #ffcc00;
$error: #e70102;
// 品牌色
$primary: #2d8cf0;
$primary-click: #2b85e4;
$primary-hover: #5cadff;
$primary-hover-dropdown: rgba($primary-hover, 0.2);
$primary-disabled: #274d75;
// 辅助色拓展
$danger-hover: #ff4d4e;
$danger-active: #d40001;
$menu-primary: #464c5b; // tab菜单主题色
// 图标颜色
$ic-white-normal: #fff;
$ic-black-normal: #4e4e4e;
$ic-hover: #a7a7a7;
$ic-disabled: #5f5f5f;
$ic-selected: #1088f2;
// 中性色-黑
$dark-bg-light: #868688; // 背景色浅色
$dark-btn-hover: #5d5f61; // 按钮 hover
$dark-border: #4f4f4f; // 边框色
$dark-btn-disabled: #3c3c3c; // 按钮主色禁用
$dark-border-secondary: #393939; // 第二边框色
$dark-basic-primary: #232323; // 第一基础底色
$dark-basic-secondary: #282828; // 第二基础底色
$dark-highlight: #232323; // 高亮色
$dark-disable: #444444; // 置灰底色
$dark-project-disabled: #292929;
// 中性色-白
$light-bg-primary: #fff; // 1 背景
$light-bg-secondary: #f7f9fa; // 2 背景
$light-divider: #e8eaec; // 3 分割线
$light-border: #dcdee2; // 4 边框
$light-disabled: #c5c8ce; // 5 失效
$light-auxiliary: #808695; // 6 辅助图标
$light-main-text: #515a6e; // 7 正文 用处
$light-title: #17233d; // 8 标题 用处
$light-bg-menu: #3b3e40; // 9 菜单栏背景色 用处
$light-border-secondary: #e8e8e8; // 第二边框色
// 字体
// 白色
$text-white-basic: #fff; // 基础色
$text-white-main: rgba($text-white-basic, 1); // 正文
$text-white-secondary: rgba($text-white-basic, 0.45); // 次级
$text-white-disabled: rgba($text-white-basic, 0.25); // 置灰
// 黑色
$text-black-basic: #000000; // 基础色
$text-black-emphasize: rgba($text-black-basic, 0.85); // 强调
$text-black-main: rgba($text-black-basic, 0.65); // 正文
$text-black-secondary: rgba($text-black-basic, 0.45); // 次要
$text-black-disabled: rgba($text-black-basic, 0.25); // 置灰
$text-link: $primary;
$text-danger: $danger;
// 标签
$tag-green: #19be6b;
// 滚动条等颜色
$scroll-bar: #c5c8ce;
$scroll-bar-dark: #5f5f5f;
// 选择框
$select-disabled: #d8d8d8;

19
src/types/enums.ts

@ -0,0 +1,19 @@
export enum ERouterName {
Element = 'element',
Project = 'project',
Tsa = 'tsa',
Layer = 'layer',
Media = 'media',
Wayline = 'wayline',
Livestream = 'livestream',
Pilot = 'pilot-login',
PilotHome = 'pilot-home',
PilotMedia = 'pilot-media',
PilotLiveshare = 'pilot-liveshare'
}
export enum EStorageKey {
LANG_CODE = 'DJI_CREATE_VITE_H5_APP:lang_code',
TEST_TOOLS_POSITION_STORAGE_KEY = 'DJI_CREATE_VITE_H5_APP:test_tools_position',
SESSION_ID = 'DJI_CREATE_VITE_H5_APP:sess'
}

1
src/types/index.ts

@ -0,0 +1 @@
export * from './enums'

6
src/types/map-enum.ts

@ -0,0 +1,6 @@
export enum MapDoodleEnum {
PIN = 'pin',
POLYLINE = 'polyline',
POLYGON = 'polygon',
Close = 'off'
}

100
src/types/map.d.ts vendored

@ -0,0 +1,100 @@
export interface MapGeographicPosition {
longitude: number;
latitude: number;
height?: number;
}
export enum LayerType {
Normal,
Default,
Share
}
export interface pinAMapPosition {
KL: number
className: string
kT: number
lng: number
lat: number
}
export enum ResourceStatus {
NotShow,
Show
}
export type GeojsonCoordinate = [number, number, number?]
export interface GeojsonLine {
type: 'Feature'
properties: {
color: string
directConnected?: boolean
}
geometry: {
type: 'LineString'
coordinates: GeojsonCoordinate[]
}
}
export interface GeojsonPolygon {
type: 'Feature'
properties: {
color: string
}
geometry: {
type: 'Polygon'
coordinates: GeojsonCoordinate[][]
}
}
export interface GeojsonPoint {
type: 'Feature'
properties: {
color: string
clampToGround?: boolean
}
geometry: {
type: 'Point'
coordinates: GeojsonCoordinate
}
}
export type GeojsonFeature = GeojsonLine | GeojsonPolygon | GeojsonPoint
interface ResourceObjectBasic {
user_name: string
user_id?: string
type:0| 1 | 2
content: unknown
}
export interface PinResource extends ResourceObjectBasic {
type: 0
content: GeojsonFeature
}
export type ResourceObject = PinResource
export enum LayerElevationLoadStatus {
Unload,
Load
}
export interface LayerResource {
id: string
name: string
order: number
status: ResourceStatus
resource: ResourceObject | null
display: number
create_time: number
elevation_load_status?: LayerElevationLoadStatus //
}
export interface Layer {
id: string
name: string
order: number
create_time: number
type: LayerType
is_distributed: boolean
is_lock: boolean
elements: null | LayerResource[],
is_check?: boolean
is_select?: boolean
}

98
src/types/mapLayer.ts

@ -0,0 +1,98 @@
import { MapElementEnum } from '/@/constants/map'
export interface mapLayerStyle {
background: string
}
export interface mapLayerChildren {
key: string
style: mapLayerStyle
title: string
obj: any
}
export interface mapLayerChildrenObj {
className: string
key: string
name: string
type: string
}
// 拖拽事件
export interface DropEvent {
node: {
eventKey: string
pos: string
$parent: any
}
dragNode: {
eventKey: string
}
dropPosition: number
dropToGap: boolean
}
export interface mapLayer {
key?: string
title: string
id: string
name: string
style: mapLayerStyle
elements: any
}
export interface elementGroupsReq{
groupId: string
isDistributed: boolean
}
export interface PostElementsBody {
id: string
name: string
resource: {
type: MapElementEnum,
user_name?: string,
content: {
type:string,
properties:{
color:string,
clampToGround:boolean
},
geometry:{
type:string,
coordinates:unknown
}
},
}
}
export interface Color {
id: number
color: string
selected: boolean,
name: string
}
export enum GeoType {
LineString = 'LineString',
Polygon = 'Polygon',
Point = 'Point'
}
export enum ResourceStatus {
NotShow,
Show
}
export enum LayerElevationLoadStatus {
Unload,
Load
}
export interface PutElementsBody {
name?: string
status?: ResourceStatus
content?: unknown
display?: number
elevation_load_status?: LayerElevationLoadStatus
}
export enum LayerType {
Normal,
Default,
Share,
Reconstruction
}

13
src/use-common-components.ts

@ -0,0 +1,13 @@
import { App, DefineComponent } from 'vue'
const components: Record<string, DefineComponent<{}, {}, any>> = {
}
export const CommonComponents = {
install (app: App): void {
Object.keys(components).forEach(name => {
app.component(name, components[name])
})
}
}

4
src/utils/data-process.ts

@ -0,0 +1,4 @@
export function formatPhoneNum (phoneNum: string | number) {
const str = String(phoneNum)
return str.substring(0, 3) + '****' + str.slice(-4)
}

81
src/utils/genjson.ts

@ -0,0 +1,81 @@
import {
MapGeographicPosition,
} from '/@/types/map'
export type GeojsonCoordinate = [number, number, number?]
export interface GeojsonLine {
type: 'Feature'
properties: {
color: string
directConnected?: boolean
}
geometry: {
type: 'LineString'
coordinates: GeojsonCoordinate[]
}
}
export interface GeojsonPolygon {
type: 'Feature'
properties: {
color: string
}
geometry: {
type: 'Polygon'
coordinates: GeojsonCoordinate[][]
}
}
export interface GeojsonPoint {
type: 'Feature'
properties: {
color: string
clampToGround?: boolean
}
geometry: {
type: 'Point'
coordinates: GeojsonCoordinate
}
}
export type GeojsonFeature = GeojsonLine | GeojsonPolygon | GeojsonPoint
export function geographic2Coordinate (position: MapGeographicPosition): GeojsonCoordinate {
const coordinates: GeojsonCoordinate = [position.longitude, position.latitude]
if (position.height !== undefined) coordinates.push(position.height)
return coordinates
}
export function generateLine (coordinates: MapGeographicPosition[], properties: GeojsonLine['properties']): GeojsonFeature {
return {
type: 'Feature',
properties,
geometry: {
type: 'LineString',
coordinates: coordinates.map(geographic2Coordinate),
},
}
}
export function generatePolygon (coordinates: MapGeographicPosition[], properties: GeojsonPolygon['properties']): GeojsonFeature {
return {
type: 'Feature',
properties,
geometry: {
type: 'Polygon',
coordinates: [coordinates.map(geographic2Coordinate)],
},
}
}
export function generatePoint (position: MapGeographicPosition, properties: GeojsonPoint['properties']): GeojsonFeature {
return {
type: 'Feature',
properties,
geometry: {
type: 'Point',
coordinates: geographic2Coordinate(position),
},
}
}

19
src/utils/layer-tree.ts

@ -0,0 +1,19 @@
const layerTreeTypes = ['layer', 'resource'] as const
type LayerTreeType = (typeof layerTreeTypes)[number]
const Spliter = '__'
export function getLayerTreeKey (type: LayerTreeType, id: number | string) {
return `${type}${Spliter}${id}`
}
export function isLayerTreeKey (key: string, type?: LayerTreeType) {
if (type) {
return key.startsWith(`${type}${Spliter}`)
} else {
return layerTreeTypes.some(t => key.startsWith(`${t}${Spliter}`))
}
}
export function getIdFromLayerTreeKey (key: string) {
return key.split(Spliter)[1]
}

28
src/utils/logger.ts

@ -0,0 +1,28 @@
/**
* Used for log printing in a non-production environment
* @param args
*/
export function consoleLog (...args: Parameters<typeof console.log>) {
if (import.meta.env.VITE_APP_ENVIRONMENT !== 'PROD') {
window.console.log.apply(null, args) // eslint-disable-line no-console
}
}
export function consoleWarn (...args: Parameters<typeof console.warn>) {
if (import.meta.env.VITE_APP_ENVIRONMENT !== 'PROD') {
console.warn.apply(null, args) // eslint-disable-line no-console
}
}
export function consoleError (...args: Parameters<typeof console.error>) {
if (import.meta.env.VITE_APP_ENVIRONMENT !== 'PROD') {
console.error.apply(null, args) // eslint-disable-line no-console
}
}
export function testEnvLog (...args: Parameters<typeof console.log>) {
if (import.meta.env.VITE_APP_ENVIRONMENT !== 'PROD') {
console.log.apply(null, args) // eslint-disable-line no-console
}
}

44
src/utils/map-layer-utils.ts

@ -0,0 +1,44 @@
import { pinAMapPosition, MapGeographicPosition, Layer, LayerType, LayerElevationLoadStatus } from '/@/types/map'
import { generatePoint, generateLine, generatePolygon } from '/@/utils/genjson'
import { MapDoodleColor, MapElementEnum } from '/@/constants/map'
function getPinPosition (pinAMapPosition: pinAMapPosition):MapGeographicPosition {
return { height: 0, latitude: pinAMapPosition.lat, longitude: pinAMapPosition.lng }
}
export function generatePointContent (pinAMapPosition: pinAMapPosition) {
const position = getPinPosition(pinAMapPosition)
return {
type: MapElementEnum.PIN,
content: generatePoint(position, {
color: MapDoodleColor.PinColor,
clampToGround: true,
})
}
}
function getLieOrPolyPosition (mapPosition: pinAMapPosition[]):MapGeographicPosition[] {
const position = [] as MapGeographicPosition[]
mapPosition.forEach(item => {
position.push({ height: 0, latitude: item.lat, longitude: item.lng })
})
return position
}
export function generateLineContent (mapPosition: pinAMapPosition[]) {
const position = getLieOrPolyPosition(mapPosition)
return {
type: MapElementEnum.LINE,
content: generateLine(position, {
color: MapDoodleColor.PolylineColor,
directConnected: false,
})
}
}
export function generatePolyContent (mapPosition: pinAMapPosition[]) {
const position = getLieOrPolyPosition(mapPosition)
return {
type: MapElementEnum.POLY,
content: generatePolygon(position, {
color: MapDoodleColor.PolygonColor,
})
}
}

42
src/utils/storage.ts

@ -0,0 +1,42 @@
import { EStorageKey } from '/@/types/enums'
import { consoleWarn } from './logger'
function getStorageData (key: EStorageKey, parse?: boolean): string | null
function getStorageData<T> (key: EStorageKey, parse?: boolean): T | null
function getStorageData (key: EStorageKey, parse?: boolean): any {
const value = window.localStorage.getItem(key)
if (parse && value) {
try {
const result = JSON.parse(value)
return result
} catch (e) {
consoleWarn('appStorage.get failed, err:', e)
return null
}
} else {
return value
}
}
function clearStorageData (key: EStorageKey | EStorageKey[]) {
let keyList: EStorageKey[] = []
if (Array.isArray(key)) {
keyList = key
} else {
keyList = [key]
}
keyList.forEach(item => {
window.localStorage.removeItem(item)
})
}
const appStorage = {
save (key: EStorageKey, value: string) {
window.localStorage.setItem(key, value)
},
get: getStorageData,
clear: clearStorageData,
}
export default appStorage

6
src/utils/uuid.ts

@ -0,0 +1,6 @@
export function uuidv4 () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}

85
src/vendors/coordtransform.js vendored

@ -0,0 +1,85 @@
/**
* Conversion between the coordinates of the National Bureau of Survey and Measurement (Mars coordinates, GCJ02) and the WGS84 coordinate system
*/
const x_PI = 3.14159265358979324 * 3000.0 / 180.0;
const PI = 3.1415926535897932384626;
const a = 6378245.0;
const ee = 0.00669342162296594323;
/**
* WGS84 to Mars coordinate system GCj02
* @param lng
* @param lat
* @returns {*[]}
*/
export function wgs84togcj02(lng, lat) {
if (out_of_china(lng, lat)) {
return [lng, lat]
}
else {
var dlat = transformlat(lng - 105.0, lat - 35.0);
var dlng = transformlng(lng - 105.0, lat - 35.0);
var radlat = lat / 180.0 * PI;
var magic = Math.sin(radlat);
magic = 1 - ee * magic * magic;
var sqrtmagic = Math.sqrt(magic);
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI);
dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI);
var mglat = lat + dlat;
var mglng = lng + dlng;
return [mglng, mglat]
}
}
/**
* GCJ02 transform WGS84
* @param lng
* @param lat
* @returns {*[]}
*/
export function gcj02towgs84(lng, lat) {
var lat = +lat;
var lng = +lng;
if (out_of_china(lng, lat)) {
return [lng, lat]
} else {
var dlat = transformlat(lng - 105.0, lat - 35.0);
var dlng = transformlng(lng - 105.0, lat - 35.0);
var radlat = lat / 180.0 * PI;
var magic = Math.sin(radlat);
magic = 1 - ee * magic * magic;
var sqrtmagic = Math.sqrt(magic);
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI);
dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI);
var mglat = lat + dlat;
var mglng = lng + dlng;
return [lng * 2 - mglng, lat * 2 - mglat]
}
}
function transformlat(lng, lat) {
var ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));
ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0;
return ret
}
export function transformlng(lng, lat) {
var ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));
ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0;
return ret
}
/**
* Judge whether you are in the country or not if you are not in the country
* @param lng
* @param lat
* @returns {boolean}
*/
function out_of_china(lng, lat) {
return (lng < 72.004 || lng > 137.8347) || ((lat < 0.8293 || lat > 55.8271) || false);
}

2
src/vendors/jswebrtc.min.js vendored

File diff suppressed because one or more lines are too long

1
src/vite-env.d.ts vendored

@ -0,0 +1 @@
/// <reference types="vite/client" />

29
tsconfig.json

@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"baseUrl": ".",
"types": ["vite/client"],
"lib": [
"esnext",
"dom"
],
"paths": {
"/@/*": [
"src/*"
],
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
, "src/vendors/coordtransform.js" ]
}

70
vite.config.ts

@ -0,0 +1,70 @@
import vue from '@vitejs/plugin-vue'
// config alias
import path from 'path'
import { ConfigEnv, defineConfig, UserConfigExport } from 'vite'
import ViteComponents, { AntDesignVueResolver } from 'vite-plugin-components'
// Introduce eslint plugin
import eslintPlugin from 'vite-plugin-eslint'
import OptimizationPersist from 'vite-plugin-optimize-persist'
import PkgConfig from 'vite-plugin-package-config'
import viteSvgIcons from 'vite-plugin-svg-icons'
import { viteVConsole } from 'vite-plugin-vconsole'
// https://vitejs.dev/config/
export default ({ command, mode }: ConfigEnv): UserConfigExport => defineConfig({
plugins: [
vue(),
eslintPlugin({
fix: true
}),
ViteComponents({
customComponentResolvers: [AntDesignVueResolver()],
}),
viteSvgIcons({
// 指定需要缓存的图标文件夹
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
// 指定symbolId格式
symbolId: 'icon-[dir]-[name]',
}),
viteVConsole({
entry: path.resolve(__dirname, './src/main.ts'), // 入口文件
// localEnabled: command === 'serve', // serve开发环境下
// enabled: command !== 'serve' || mode === 'test', // 打包环境下/发布测试包,
config: { // vconsole 配置项
maxLogNumber: 1000,
theme: 'light'
}
}),
PkgConfig(),
OptimizationPersist()
// [svgBuilder('./src/assets/icons/')] // All svg under src/icons/svg/ have been imported here, no need to import separately
],
server: {
open: true,
host: '0.0.0.0',
port: 8080
},
envDir: './env',
resolve: {
alias: [{
// https://github.com/vitejs/vite/issues/279#issuecomment-635646269
find: '/@',
replacement: path.resolve(__dirname, './src'),
}
]
},
css: {
preprocessorOptions: {
scss: {
// example : additionalData: `@import "./src/design/styles/variables";`
// dont need include file extend .scss
additionalData: '@import "./src/styles/variables";'
},
}
},
base: '/',
build: {
target: ['es2015'], // 最低支持 es2015
sourcemap: true
}
})

3927
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save