├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ ├── ci.yml │ └── docker.yml ├── .gitignore ├── .gitlab-ci.yml ├── .travis.yml ├── .vscode ├── extensions.json └── settings.json ├── Dockerfile ├── Dockerfile-deploy ├── LICENSE ├── Makefile ├── README.md ├── assets ├── favicon.ico ├── icons │ ├── ambulance.svg │ ├── barometer.svg │ ├── battery-charging.svg │ ├── battery-horizontal.svg │ ├── battery.svg │ ├── chart.svg │ ├── check-flag.svg │ ├── compass.svg │ ├── connected.svg │ ├── control-panel.svg │ ├── depot-blue.svg │ ├── depot.svg │ ├── disconnected.svg │ ├── doc-add.svg │ ├── doc-edit.svg │ ├── doc.svg │ ├── drone-blue.svg │ ├── drone-marker-amap.svg │ ├── drone-marker-google.svg │ ├── drone.svg │ ├── electrical.svg │ ├── electricity.svg │ ├── expand.svg │ ├── gear.svg │ ├── height.svg │ ├── info-circle.svg │ ├── land.svg │ ├── lightning-bolt.svg │ ├── maintenance.svg │ ├── map-marker.svg │ ├── map-waypoint-blue.svg │ ├── map-waypoint.svg │ ├── monitor.svg │ ├── new-view.svg │ ├── paper-busy.svg │ ├── reset.svg │ ├── robot-arm.svg │ ├── satellite.svg │ ├── signal.svg │ ├── speaker.svg │ ├── speed.svg │ ├── takeoff.svg │ ├── tasks-blue.svg │ ├── tasks.svg │ ├── timespan.svg │ ├── trumpet.svg │ ├── updown.svg │ ├── user.svg │ ├── views.svg │ ├── voltage.svg │ ├── warning.svg │ ├── wind-0.svg │ └── wind-1.svg ├── images │ ├── login-backgound.jpg │ ├── logo-100.png │ ├── logo.png │ └── staging-tip.svg ├── robots.txt └── videos │ ├── aerial0-10s.mp4 │ ├── aerial1-10s.mp4 │ ├── aerial2-10s.mp4 │ ├── aerial3-10s.mp4 │ ├── aerial4-10s.mp4 │ ├── aerial5-10s.mp4 │ └── aerial6-10s.mp4 ├── compose.yml ├── doc ├── CHANGELOG.md ├── DEVELOPMENT.md ├── README_old.md ├── SDWC-v1.jpg ├── SDWC-v2.gif ├── SDWC-v3.gif └── mqtt_doc.md ├── jsconfig.json ├── package.json ├── scripts ├── build-tar.sh └── webpack.config.js ├── src ├── App.vue ├── api │ ├── api-types.d.ts │ ├── caiyun.js │ ├── heweather.js │ ├── load-script.js │ ├── mqtt.js │ ├── plan-runnable.js │ ├── sdwc.js │ ├── super-dock-v3.js │ ├── super-dock.js │ └── wretch-jsonp.js ├── components │ ├── battery.vue │ ├── card.vue │ ├── control.vue │ ├── countdown-button.vue │ ├── custom │ │ ├── custom-item.vue │ │ └── custom.vue │ ├── debug.vue │ ├── job-file │ │ ├── iframe-doc.vue │ │ ├── iframe.vue │ │ ├── image.vue │ │ ├── job-file.vue │ │ ├── photosphere.vue │ │ ├── text.vue │ │ ├── unknown.vue │ │ └── url.vue │ ├── map │ │ ├── amap.vue │ │ ├── common.js │ │ ├── google.vue │ │ ├── map.vue │ │ ├── mapbox-loader.js │ │ ├── mapbox.vue │ │ └── node-map.vue │ ├── monitor │ │ ├── flv.vue │ │ ├── hls.vue │ │ ├── iframe.vue │ │ ├── img.vue │ │ ├── monitor.vue │ │ ├── node-monitor.vue │ │ ├── webrtc-base.vue │ │ ├── webrtc-client.js │ │ ├── webrtc-srs-client.js │ │ ├── webrtc-srs.vue │ │ ├── webrtc.vue │ │ ├── webrtc2-client.js │ │ ├── webrtc2.vue │ │ ├── webrtc3-client.js │ │ ├── webrtc3.vue │ │ ├── webrtc4-client.js │ │ └── webrtc4.vue │ ├── plan-dialog │ │ ├── plan-dialog-item.vue │ │ └── plan-dialog.vue │ ├── plan │ │ ├── editable.vue │ │ ├── plan-extra.vue │ │ ├── plan-files.vue │ │ └── readonly.vue │ ├── schedule │ │ ├── schedule-detail.vue │ │ ├── schedule-edit-advanced.vue │ │ └── schedule-edit-simple.vue │ ├── sd-icon.vue │ ├── settings │ │ ├── node-parameters.vue │ │ ├── parameter-group.vue │ │ ├── settings-input.vue │ │ ├── settings-radio.vue │ │ ├── settings-slider.vue │ │ ├── settings-switch.vue │ │ └── settings.vue │ ├── slide-confirm.vue │ ├── speaker │ │ ├── recorder.js │ │ └── speaker.vue │ ├── status │ │ ├── depot-status.vue │ │ ├── drone-status.vue │ │ ├── status-meter.vue │ │ ├── status-notify-item.vue │ │ ├── status-notify.vue │ │ └── status.vue │ └── weather │ │ ├── alert-badge.vue │ │ ├── rain-chart.vue │ │ ├── weather.vue │ │ ├── wind-chart.vue │ │ └── wind-icon.vue ├── config.json ├── constants │ ├── browser.js │ ├── drone-place-style.js │ ├── node-notification-levels.js │ ├── node-status-class.js │ ├── plan-dialog-level-class.js │ └── rpc-status-class.js ├── i18n │ ├── common.js │ ├── index.js │ └── locale │ │ ├── en.js │ │ └── zh.js ├── index.d.ts ├── main.js ├── pages │ ├── embedded.vue │ ├── iframe.vue │ ├── login.vue │ ├── node │ │ └── node.vue │ ├── overview │ │ ├── overview-map.vue │ │ ├── overview-notify.vue │ │ ├── overview-popover.vue │ │ └── overview.vue │ ├── panel │ │ ├── aside.vue │ │ ├── header.vue │ │ ├── panel.vue │ │ └── team-dialog.vue │ ├── plan │ │ ├── common.js │ │ ├── edit.vue │ │ ├── list.vue │ │ ├── new.vue │ │ ├── plan.vue │ │ └── view.vue │ └── schedule │ │ ├── edit.vue │ │ ├── list.vue │ │ ├── new.vue │ │ ├── schedule.vue │ │ └── view.vue ├── router.js ├── sdwc.d.ts ├── store │ ├── actions.js │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── config.js │ │ ├── index.js │ │ ├── node.js │ │ ├── notification.js │ │ ├── plan.js │ │ ├── preference.js │ │ ├── schedule.js │ │ ├── ui.js │ │ └── user.js ├── styles │ ├── chartist.css │ ├── development.css │ └── global.css └── util │ ├── browser-hacks.js │ ├── create-element.js │ ├── element.js │ ├── plugin-mqtt.js │ ├── wait-selector.js │ └── waypoint-parser.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* global module */ 2 | module.exports = { 3 | ignorePatterns: [ 4 | '*.d.ts' 5 | ], 6 | parserOptions: { 7 | ecmaVersion: 'latest' 8 | }, 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:vue/base', 12 | 'plugin:vue/essential' 13 | ], 14 | rules: { 15 | 'require-atomic-updates': 'off', 16 | 'no-console': 'error', 17 | 'semi': ['error', 'always'], 18 | 'quotes': ['error', 'single'] 19 | }, 20 | globals: { 21 | __SDWC__VERSION__: 'readonly', 22 | __SDWC_DEV__: 'readonly' 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: '18' 16 | cache: 'yarn' 17 | 18 | - name: Build Releases 19 | if: startsWith(github.ref, 'refs/tags/') 20 | run: ./scripts/build-tar.sh 21 | 22 | - name: Upload Release 23 | uses: softprops/action-gh-release@v1 24 | if: startsWith(github.ref, 'refs/tags/') 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | with: 28 | files: file.tar.xz 29 | draft: true 30 | prerelease: true 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Build container image 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build: 10 | name: Build container image 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Docker meta 17 | id: meta 18 | uses: docker/metadata-action@v4 19 | with: 20 | images: | 21 | ghcr.io/sb-im/sdwc 22 | registry.cn-hangzhou.aliyuncs.com/sbim/sdwc 23 | tags: | 24 | type=ref,event=branch 25 | type=semver,pattern={{version}} 26 | type=semver,pattern={{major}}.{{minor}} 27 | type=sha 28 | flavor: | 29 | latest=auto 30 | 31 | - name: Set up QEMU 32 | uses: docker/setup-qemu-action@v2 33 | 34 | - name: Set up Docker Buildx 35 | uses: docker/setup-buildx-action@v2 36 | 37 | - name: Login to GitHub Container Registry 38 | uses: docker/login-action@v2 39 | with: 40 | registry: ghcr.io 41 | username: ${{ github.actor }} 42 | password: ${{ secrets.GITHUB_TOKEN }} 43 | 44 | - name: Login to Custom Container Registry 45 | uses: docker/login-action@v2 46 | with: 47 | registry: ${{ secrets.CUSTOM_REGISTRY }} 48 | username: ${{ secrets.CUSTOM_USERNAME }} 49 | password: ${{ secrets.CUSTOM_PASSWORD }} 50 | 51 | - name: Build and push 52 | uses: docker/build-push-action@v4 53 | with: 54 | file: Dockerfile 55 | push: true 56 | context: . 57 | platforms: linux/amd64,linux/arm64 58 | tags: ${{ steps.meta.outputs.tags }} 59 | labels: ${{ steps.meta.outputs.labels }} 60 | cache-from: type=registry,ref=user/app:latest 61 | cache-to: type=inline 62 | 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | old/bower_components/ 4 | dist/ 5 | build/ 6 | npm-debug.log 7 | yarn-error.log 8 | 9 | /config.json 10 | 11 | # Editor directories and files 12 | .idea 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | .vscode 18 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:latest 2 | 3 | stages: 4 | - build 5 | - deploy 6 | 7 | #使用CI的yarn生成静态文件 8 | build: 9 | stage: build 10 | script: 11 | - rm -rf dist/ 12 | - yarn install 13 | - yarn build 14 | - cp src/config.json dist/ 15 | artifacts: 16 | paths: 17 | - dist/ 18 | 19 | #生成支持amd64/arm64平台的镜像 20 | deploy: 21 | stage: deploy 22 | image: jdrouet/docker-with-buildx:stable 23 | services: 24 | - docker:dind 25 | before_script: 26 | - docker login -u $CI_CUSTOMREGISTRY_USER -p $CI_CUSTOMREGISTRY_PASS $CI_CUSTOMREGISTRY_IMAGEBASE 27 | variables: 28 | CONTAINER_REGISTRY_IMAGE: $CI_CUSTOMREGISTRY_IMAGEBASE/sbim/sdwc 29 | script: 30 | #默认的Dockerfile构建arm64特别慢,直接用CI的dist目录复制到amd64镜像中 31 | - rm Dockerfile 32 | - mv Dockerfile-deploy Dockerfile 33 | - docker buildx create --name builderx 34 | - docker buildx use builderx 35 | - docker buildx inspect --bootstrap 36 | - docker buildx build --platform linux/amd64,linux/arm64 --tag $CONTAINER_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA --tag $CONTAINER_REGISTRY_IMAGE:latest --push . 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "node" 5 | 6 | cache: 7 | yarn: true 8 | directories: 9 | - node_modules 10 | 11 | script: 12 | - yarn test 13 | - yarn build 14 | 15 | before_deploy: 16 | - ls -Al 17 | - mv assets dist/ 18 | - mv dist /tmp/ 19 | - ls -A | xargs rm -rf 20 | - wget https://raw.githubusercontent.com/SB-IM/SDWC/gh-pages/config.json 21 | - mv /tmp/dist/* . 22 | - rmdir /tmp/dist 23 | - ls -Al 24 | 25 | 26 | deploy: 27 | provider: pages 28 | fqdn: test.newbe.cc 29 | skip-cleanup: true 30 | github-token: $GITHUB_TOKEN 31 | keep-history: false 32 | on: 33 | branch: dev 34 | 35 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [ 6 | "EditorConfig.EditorConfig", 7 | "dbaeumer.vscode-eslint", 8 | "stylelint.vscode-stylelint", 9 | "mrmlnc.vscode-stylefmt", 10 | "octref.vetur", 11 | "antfu.i18n-ally" 12 | ], 13 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 14 | "unwantedRecommendations": [] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": true, 3 | "editor.tabSize": 2, 4 | "stylelint.config": { 5 | "rules": { 6 | "indentation": 2, 7 | "string-quotes": "single" 8 | } 9 | }, 10 | "stylefmt.useStylelintConfigOverrides": true, 11 | "vetur.format.options.tabSize": 2, 12 | "vetur.format.defaultFormatter.html": "prettyhtml", 13 | "vetur.format.defaultFormatterOptions": { 14 | "prettyhtml": { 15 | "printWidth": 100, 16 | "singleQuote": false, 17 | "wrapAttributes": false, 18 | "sortAttributes": false 19 | } 20 | }, 21 | "i18n-ally.enabledParsers": [ 22 | "js" 23 | ], 24 | "i18n-ally.localesPaths": [ 25 | "src/i18n/locale" 26 | ], 27 | "i18n-ally.pathMatcher": "{locale}.js", 28 | "i18n-ally.keystyle": "nested", 29 | "i18n-ally.sourceLanguage": "zh" 30 | } 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine as build-stage 2 | RUN apk update && apk add --no-cache git 3 | WORKDIR /app 4 | COPY package*.json yarn.lock ./ 5 | RUN yarn install 6 | COPY . . 7 | RUN yarn build; \ 8 | cp src/config.json dist 9 | 10 | FROM nginx:stable-alpine as production-stage 11 | COPY --from=build-stage /app/dist /usr/share/nginx/html 12 | EXPOSE 80 13 | CMD ["nginx", "-g", "daemon off;"] 14 | -------------------------------------------------------------------------------- /Dockerfile-deploy: -------------------------------------------------------------------------------- 1 | FROM nginx:stable-alpine 2 | COPY dist /usr/share/nginx/html 3 | EXPOSE 80 4 | CMD ["nginx", "-g", "daemon off;"] 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Project image repo. 2 | IMAGE?=ghcr.io/sb-im/sdwc:latest 3 | 4 | .PHONY: image 5 | image: 6 | @docker build -t $(IMAGE) . 7 | 8 | .PHONY: push 9 | push: 10 | @docker push $(IMAGE) 11 | 12 | .PHONY: run 13 | run: 14 | @docker run \ 15 | -d \ 16 | --rm \ 17 | --name sdwc \ 18 | -p 8080:80 \ 19 | $(IMAGE) 20 | 21 | .PHONY: stop 22 | stop: 23 | @docker stop sdwc 24 | 25 | dist.tar.gz: 26 | @docker cp sdwc:/usr/share/nginx/html dist 27 | @tar -zcvf dist.tar.gz dist 28 | 29 | .PHONY: clean 30 | clean: 31 | @rm -rf dist dist.tar.gz 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SDWC == S Dashboard Web Console 2 | 3 | [![ci](https://github.com/sb-im/sdwc/actions/workflows/ci.yml/badge.svg)](https://github.com/sb-im/sdwc/actions/workflows/ci.yml) 4 | 5 | Robotics Cluster Cloud Console 6 | 7 | ## Introduction 8 | 9 | > The Application developed by the sbim inno (StrawBerry Tech) 10 | > MPL-2.0 protocol open source, the goal is to do a universal robotics console application 11 | 12 | ## Build Setup 13 | 14 | ``` bash 15 | # install dependencies 16 | yarn install 17 | 18 | # build for production with minification 19 | yarn run build 20 | ``` 21 | 22 | ## Install && Config 23 | 24 | ```bash 25 | cp src/config.json ./ 26 | ``` 27 | 28 | ## Development 29 | 30 | ```bash 31 | # docker or nerdctl 32 | docker compose up 33 | 34 | # serve with hot reload at localhost:8080 35 | yarn run dev 36 | ``` 37 | 38 | ## CHANGELOG 39 | 40 | [doc/CHANGELOG.md](/doc/CHANGELOG.md) 41 | 42 | ## Development Docs 43 | 44 | [doc/DEVELOPMENT.md](/doc/DEVELOPMENT.md) 45 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb-im/sdwc/b404f90a0b99600e949ec9eecd9621c82fe54e6a/assets/favicon.ico -------------------------------------------------------------------------------- /assets/icons/ambulance.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/barometer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/battery-charging.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/battery-horizontal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/battery.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/check-flag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/compass.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/connected.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/control-panel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/depot-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/icons/depot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/icons/disconnected.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/doc-add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/doc-edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/doc.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/drone-blue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/drone-marker-amap.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/drone-marker-google.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/icons/drone.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/electrical.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/electricity.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/expand.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/height.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/info-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/land.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/lightning-bolt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/maintenance.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/map-marker.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/map-waypoint-blue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/map-waypoint.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/monitor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Untitled 3 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /assets/icons/new-view.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/paper-busy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/reset.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/robot-arm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/satellite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/signal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/speaker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/icons/speed.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/takeoff.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/tasks-blue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/tasks.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/timespan.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/trumpet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/icons/updown.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/views.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/voltage.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/warning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/wind-0.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/icons/wind-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/images/login-backgound.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb-im/sdwc/b404f90a0b99600e949ec9eecd9621c82fe54e6a/assets/images/login-backgound.jpg -------------------------------------------------------------------------------- /assets/images/logo-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb-im/sdwc/b404f90a0b99600e949ec9eecd9621c82fe54e6a/assets/images/logo-100.png -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb-im/sdwc/b404f90a0b99600e949ec9eecd9621c82fe54e6a/assets/images/logo.png -------------------------------------------------------------------------------- /assets/images/staging-tip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Development 14 | 15 | -------------------------------------------------------------------------------- /assets/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /assets/videos/aerial0-10s.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb-im/sdwc/b404f90a0b99600e949ec9eecd9621c82fe54e6a/assets/videos/aerial0-10s.mp4 -------------------------------------------------------------------------------- /assets/videos/aerial1-10s.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb-im/sdwc/b404f90a0b99600e949ec9eecd9621c82fe54e6a/assets/videos/aerial1-10s.mp4 -------------------------------------------------------------------------------- /assets/videos/aerial2-10s.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb-im/sdwc/b404f90a0b99600e949ec9eecd9621c82fe54e6a/assets/videos/aerial2-10s.mp4 -------------------------------------------------------------------------------- /assets/videos/aerial3-10s.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb-im/sdwc/b404f90a0b99600e949ec9eecd9621c82fe54e6a/assets/videos/aerial3-10s.mp4 -------------------------------------------------------------------------------- /assets/videos/aerial4-10s.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb-im/sdwc/b404f90a0b99600e949ec9eecd9621c82fe54e6a/assets/videos/aerial4-10s.mp4 -------------------------------------------------------------------------------- /assets/videos/aerial5-10s.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb-im/sdwc/b404f90a0b99600e949ec9eecd9621c82fe54e6a/assets/videos/aerial5-10s.mp4 -------------------------------------------------------------------------------- /assets/videos/aerial6-10s.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb-im/sdwc/b404f90a0b99600e949ec9eecd9621c82fe54e6a/assets/videos/aerial6-10s.mp4 -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | redis: 4 | image: redis:6-alpine 5 | container_name: redis 6 | hostname: redis 7 | 8 | emqx: 9 | image: emqx/emqx:latest 10 | container_name: emqx 11 | hostname: mqtt-broker 12 | environment: 13 | - EMQX_LOADED_PLUGINS="emqx_recon,emqx_retainer,emqx_management,emqx_auth_mnesia,emqx_auth_jwt,emqx_auth_redis" 14 | - EMQX_ALLOW_ANONYMOUS=false 15 | - EMQX_AUTH__REDIS__SERVER=redis:6379 16 | - EMQX_AUTH__REDIS__POOL=8 17 | - EMQX_AUTH__REDIS__DATABASE=1 18 | - EMQX_AUTH__USER__1__USERNAME=${MQTT_USERNAME:-admin} 19 | - EMQX_AUTH__USER__1__PASSWORD=${MQTT_PASSWORD:-public} 20 | - EMQX_AUTH__JWT__SECRET=secretkey 21 | - EMQX_AUTH__JWT__FROM=password 22 | ports: 23 | - "1883:1883/tcp" 24 | - "8083:8083/tcp" 25 | 26 | postgres: 27 | image: postgres:14 28 | container_name: postgres 29 | hostname: postgres 30 | environment: 31 | POSTGRES_DB: gosd 32 | POSTGRES_USER: postgres 33 | POSTGRES_PASSWORD: password 34 | 35 | gosd: 36 | image: ghcr.io/sb-im/gosd:3.0 37 | container_name: gosd 38 | depends_on: 39 | - emqx 40 | - redis 41 | - postgres 42 | environment: 43 | DEMO_MODE: true 44 | LISTEN_ADDR: '0.0.0.0:8000' 45 | REDIS_URL: 'redis://redis:6379/1' 46 | MQTT_URL: 'mqtt://admin:public@mqtt-broker:1883' 47 | DATABASE_URL: 'postgres://postgres:password@postgres/gosd?sslmode=disable' 48 | ports: 49 | - "8000:8000/tcp" 50 | restart: always 51 | 52 | ncp-1: 53 | image: ghcr.io/sb-im/ncp:latest 54 | container_name: ncp-1 55 | environment: 56 | NCP_UUID: 1 57 | NCP_TYPE: drone 58 | NCP_MQTT_URL: 'mqtt://admin:public@mqtt-broker:1883' 59 | 60 | ncp-2: 61 | image: ghcr.io/sb-im/ncp:latest 62 | container_name: ncp-2 63 | environment: 64 | NCP_UUID: 2 65 | NCP_TYPE: depot 66 | NCP_MQTT_URL: 'mqtt://admin:public@mqtt-broker:1883' 67 | 68 | ncp-3: 69 | image: ghcr.io/sb-im/ncp:latest 70 | container_name: ncp-3 71 | environment: 72 | NCP_UUID: 3 73 | NCP_TYPE: drone 74 | NCP_MQTT_URL: 'mqtt://admin:public@mqtt-broker:1883' 75 | 76 | networks: 77 | default: 78 | name: superdock 79 | 80 | -------------------------------------------------------------------------------- /doc/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # SDWC - Changelog 2 | 3 | ## 2023-01-04 v4.0.0 4 | 5 | Use api/v3 6 | 7 | ### Add 8 | - Layout 9 | - config sidebar 10 | - Task 11 | - WPML format airline 12 | - Speaker 13 | - speaker record audio 14 | - Map 15 | - gimbal direction 16 | - Monitor 17 | - SRS webrtc 18 | - Status 19 | - Setting Page 20 | 21 | ### Change 22 | - Weather 23 | - caiyun => heweather 24 | 25 | ### Fix 26 | - Monitor 27 | - gimbal target and current show 28 | - click drift 29 | - Status 30 | - charger_info 31 | - Debug 32 | - jsonrpc single number 33 | 34 | ## 2020-07-24 v3.5.0 35 | 36 | ### Add 37 | - Overview 38 | - show place markers on map 39 | - notify popup and history 40 | 41 | - Map 42 | - Add mapbox 43 | - Heatmap support 44 | 45 | - Monitor 46 | - joystick toggle 47 | 48 | - Add custom 49 | - router login api 50 | 51 | ### Change 52 | - API preflight 53 | 54 | ### Fix 55 | - node loading spinner not centered on ipad 56 | - Map 57 | - map follow, dragged 58 | - google, mapbox 59 | 60 | - Preflight 61 | - background on safari 62 | - touch screen support 63 | - mqtt keepalive 64 | 65 | ## 2020-04-07 v3.4.0 66 | 67 | ### 新增 68 | 69 | - 顶部工具栏 70 | - 记录通知历史 71 | - 点击通讯状态中的节点名称,快速导航到节点页面 72 | 73 | - 侧边栏 74 | - 可折叠式侧边栏 75 | - 根据窗口或屏幕大小自动折叠或展开 76 | 77 | - 通用组件 78 | - 视频监控的全屏按钮与其他界面元素风格一致 79 | - 地图中可展示无人机朝向 80 | - 高级控制按钮可指示任务执行状态 81 | - 调试组件可指定命令参数 82 | 83 | - Login 登录页面 84 | - 用户名输入框自动聚焦,按回车可切换焦点到密码输入框,或直接登录 85 | - 可配置在底部展示备案信息 86 | 87 | - Overview 概况页面 88 | - 展示无人机/机场/任务数量及总览地图 89 | - 总览地图可自动适应显示范围 90 | 91 | - Plan 任务页面 92 | - 新增起飞前检测机制,包括节点状态及实时天气。检测通过后滑动滑块确认执行任务,并自动跳转到机场页 93 | - 执行/停止任务后显示通知 94 | - 页面见切换时,平滑切换路径点地图 95 | - 任务执行历史可排序及分页展示 96 | 97 | - Depot 机场页面 98 | - 新增机场状态条,包含供电/舱门等状态,及充电数据图表 99 | - 更精确的历史天气图表 100 | 101 | - Drone 无人机页面 102 | - 视频监控画面中可通过鼠标拖动/触屏滑动来控制俯仰角 103 | - 卫星地图可切换自由视角或跟踪视角 104 | - 卫星地图可手动清除轨迹,限制路径点数量为 1024 个 105 | - 卫星地图可配置展示多个标记点,可选无人机到标记点的预计飞行路线 106 | - 卫星地图鼠标右键/触屏长按,实时下达控制指令 107 | 108 | - 其它 109 | - 所有界面及操作适配 iPad 110 | - 统一界面图标风格 111 | - 登录超时后自动退回到登录页面 112 | - 优化英文界面翻译 113 | 114 | ## 2019-08-16 v3.3.0 115 | 116 | ### 新增 117 | 118 | - **完全重构** 119 | - 按需动态加载控制组件 120 | - 刷新页面后保留登录状态 121 | - 执行 MQTT 调用后显示通知 122 | - 可选高德地图作为地图源 123 | - 当屏幕宽度大于 1200px 时使用双栏布局 124 | - 实时天气组件 125 | - 动态调试组件 126 | - 智能电池组件 127 | 128 | ## 2019-05-21 v3.2.2 129 | 130 | ### 新增 131 | 132 | - WebRTC 显示错误信息及自动重连 133 | 134 | ### 修复 135 | 136 | - 无人机控制模式及调试组件修复 137 | 138 | ## 2019-04-22 v3.2.1 139 | 140 | ### 新增 141 | 142 | - 自定义 WebRTC ICE Server 143 | 144 | ### 修复 145 | 146 | - GPS 状态名称调整 147 | - 无人机状态文本调整 148 | 149 | ## 2019-04-20 v3.2.0 150 | 151 | ### 新增 152 | 153 | - 下载任务地图及其他附加文件 154 | - 显示无人机状态及飞行路线 155 | - 控制无人机 gimbal mode and angle 156 | - 添加无人机“自稳模式” 157 | - WebRTC 视频串流 158 | 159 | ### 修复 160 | 161 | - 任务编辑 401 错误 162 | - 天气预报不可用 163 | - JSON-RPC 默认参数应为空数组 164 | 165 | ## 2019-03-28 v3.1.0 166 | 167 | ### 新增 168 | 169 | - 未登录时重定向所有访问到登录页面 170 | - MQTT 通信模块 171 | - 顶部操作栏中显示用户邮箱 172 | - 顶部操作栏中显示节点通信状态 173 | 174 | ### 修复 175 | 176 | - 任务查看 401 错误 177 | -------------------------------------------------------------------------------- /doc/SDWC-v1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb-im/sdwc/b404f90a0b99600e949ec9eecd9621c82fe54e6a/doc/SDWC-v1.jpg -------------------------------------------------------------------------------- /doc/SDWC-v2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb-im/sdwc/b404f90a0b99600e949ec9eecd9621c82fe54e6a/doc/SDWC-v2.gif -------------------------------------------------------------------------------- /doc/SDWC-v3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sb-im/sdwc/b404f90a0b99600e949ec9eecd9621c82fe54e6a/doc/SDWC-v3.gif -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": [ 6 | "./src/*" 7 | ], 8 | "assets/*": [ 9 | "./assets/*" 10 | ] 11 | } 12 | }, 13 | "include": [ 14 | "src/**/*" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sb-im/sdwc", 3 | "description": "Multiple drones and ground station Web consoles", 4 | "version": "4.0.3", 5 | "author": "Rocka ", 6 | "contributors": [ 7 | "metal A-wing ", 8 | "gitgitgogogo ", 9 | "william " 10 | ], 11 | "license": "MPL-2.0", 12 | "private": true, 13 | "scripts": { 14 | "dev": "webpack serve --config scripts/webpack.config.js --env mode=development --env dev", 15 | "build": "webpack --config scripts/webpack.config.js --env mode=production", 16 | "test": "eslint --ext .js,.vue src/" 17 | }, 18 | "dependencies": { 19 | "@google/markerwithlabel": "^1.2.12", 20 | "@tinyhttp/content-disposition": "^2.0.7", 21 | "bowser": "^2.11.0", 22 | "buffer": "^6.0.3", 23 | "chartist": "^0.11.4", 24 | "chartist-plugin-tooltips": "^0.0.17", 25 | "coordtransform": "^2.1.2", 26 | "cron-parser": "^4.6.0", 27 | "ele-vue-cron": "^0.1.4", 28 | "element-ui": "^2.15.10", 29 | "eventemitter3": "^4.0.7", 30 | "fetch-jsonp": "^1.2.3", 31 | "flv.js": "^1.6.2", 32 | "hls.js": "^1.2.4", 33 | "ifvisible.js": "^2.0.11", 34 | "jsonrpc-lite": "^2.2.0", 35 | "lodash": "^4.17.20", 36 | "mapbox-gl": "^1.13.1", 37 | "material-design-icons-iconfont": "^6.7.0", 38 | "mqtt": "^4.3.7", 39 | "nipplejs": "^0.10.0", 40 | "papaparse": "^5.3.2", 41 | "photo-sphere-viewer": "^4.7.3", 42 | "uzip": "^0.20201231.0", 43 | "vue": "2.6.14", 44 | "vue-i18n": "^8.28.2", 45 | "vue-json-tree-view": "^2.1.6", 46 | "vue-router": "^3.6.5", 47 | "vuex": "^3.6.2", 48 | "wretch": "^2.1.5" 49 | }, 50 | "devDependencies": { 51 | "@babel/core": "^7.19.6", 52 | "@babel/preset-env": "^7.19.4", 53 | "@types/chartist": "^0.11.1", 54 | "@types/mapbox-gl": "^1.13.2", 55 | "babel-loader": "^9.0.1", 56 | "babel-plugin-component": "^1.1.1", 57 | "copy-webpack-plugin": "^11.0.0", 58 | "core-js": "^3.26.0", 59 | "css-loader": "^6.5.1", 60 | "css-minimizer-webpack-plugin": "^4.2.2", 61 | "eslint": "^8.26.0", 62 | "eslint-plugin-vue": "^9.7.0", 63 | "html-webpack-plugin": "^5.5.0", 64 | "mini-css-extract-plugin": "^2.6.1", 65 | "style-loader": "^3.3.1", 66 | "vue-loader": "15.9.8", 67 | "vue-template-compiler": "2.6.14", 68 | "webpack": "^5.74.0", 69 | "webpack-cli": "^4.10.0", 70 | "webpack-dev-server": "^4.11.1" 71 | }, 72 | "resolutions": { 73 | "**/async-validator": "^4.2.5", 74 | "**/vue": "2.6.14" 75 | }, 76 | "browserslist": [ 77 | "Chrome > 89", 78 | "Firefox > 87", 79 | "Safari > 14" 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /scripts/build-tar.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ev 3 | 4 | yarn install 5 | yarn test 6 | yarn build "$@" 7 | 8 | tar -cJf file.tar.xz -C dist/ . 9 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/api/api-types.d.ts: -------------------------------------------------------------------------------- 1 | namespace ApiTypes { 2 | export interface LoginResponseOk { 3 | access_token: string; 4 | token_type: string; 5 | expires_in: number; 6 | created_at: number; 7 | } 8 | 9 | export interface LoginResponseErr { 10 | error: string; 11 | error_description: string; 12 | } 13 | 14 | namespace V3 { 15 | export interface ServerStatus { 16 | build_date: string; 17 | language: string; 18 | time: string; 19 | timezone: string; 20 | version: string; 21 | } 22 | 23 | export interface Login { 24 | code: number; 25 | /** error message */ 26 | message?: string; 27 | expire?: string; 28 | token?: string; 29 | } 30 | 31 | export interface SwitchTeam { 32 | expire?: string; 33 | token?: string; 34 | /** error message */ 35 | error?: string; 36 | } 37 | 38 | export interface BaseEntity { 39 | id: number; 40 | created_at: string; 41 | updated_at: string; 42 | } 43 | 44 | export interface Team extends BaseEntity { 45 | name: string; 46 | } 47 | 48 | export interface User extends BaseEntity { 49 | language: string; 50 | team_id: number; 51 | teams: Team[]; 52 | timezone: string; 53 | username: string; 54 | } 55 | 56 | export interface Point { 57 | name: string; 58 | type: string; 59 | params: any; 60 | node_id?: string; 61 | } 62 | 63 | export interface Node { 64 | id: string; 65 | uuid: string; 66 | name: string; 67 | points?: Point[]; 68 | } 69 | 70 | export interface Task extends BaseEntity { 71 | name: string; 72 | index: number; 73 | node_id: string; 74 | files?: { [key: string]: string }; 75 | extra?: { [key: string]: string }; 76 | } 77 | 78 | export interface Job extends BaseEntity { 79 | /** job ordinal number */ 80 | index: number; 81 | files?: { [key: string]: string }; 82 | extra?: { [key: string]: string }; 83 | } 84 | 85 | export interface TaskWithJobs extends Task { 86 | jobs: Job[]; 87 | } 88 | 89 | export interface TaskWithJob extends Task { 90 | job: Job; 91 | } 92 | 93 | export interface Blob extends BaseEntity { 94 | name: string; 95 | uxid: string; 96 | } 97 | 98 | export interface Schedule extends BaseEntity { 99 | name: string; 100 | /** 101 | * cron expression https://pkg.go.dev/github.com/robfig/cron/v3#hdr-Usage 102 | */ 103 | cron: string; 104 | enable: boolean; 105 | method: string; 106 | params: string; 107 | } 108 | 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/api/caiyun.js: -------------------------------------------------------------------------------- 1 | import wretchJSONP from './wretch-jsonp'; 2 | import queryString from 'wretch/addons/queryString'; 3 | 4 | const baseWr = wretchJSONP.addon(queryString).url('https://api.caiyunapp.com/v2/'); 5 | 6 | /** 7 | * test token 8 | * @see https://open.caiyunapp.com/实况天气接口 9 | */ 10 | let wr = baseWr.url('TAkhjf8d1nlSlspN'); 11 | 12 | export function setApiKey(key) { 13 | wr = baseWr.url(key); 14 | } 15 | 16 | /** 17 | * @param {string} url 18 | */ 19 | function request(url) { 20 | return wr.url(url) 21 | .query({ unit: 'metric:v2' }) 22 | .get() 23 | .json() 24 | .then(res => { 25 | if (res.status === 'ok') return res; 26 | else throw res; 27 | }); 28 | } 29 | 30 | /** 31 | * 分钟级降雨预报 32 | * @see https://open.caiyunapp.com/分钟级降雨预报接口 33 | * @param {number} lng 34 | * @param {number} lat 35 | */ 36 | export function minutely(lng, lat) { 37 | return request(`/${lng},${lat}/minutely`); 38 | } 39 | -------------------------------------------------------------------------------- /src/api/heweather.js: -------------------------------------------------------------------------------- 1 | import wretch from 'wretch'; 2 | import queryString from 'wretch/addons/queryString'; 3 | 4 | let BaseURL = 'https://api.heweather.net'; 5 | 6 | if (__SDWC_DEV__) { 7 | BaseURL = 'https://devapi.heweather.net'; 8 | } 9 | 10 | const wr = wretch(BaseURL).addon(queryString); 11 | 12 | const conf = { 13 | key: '', 14 | lang: '', 15 | unit: 'm' 16 | }; 17 | 18 | export function setApiKey(key) { 19 | conf.key = key; 20 | } 21 | 22 | export function setLanguage(lang) { 23 | conf.lang = lang; 24 | } 25 | 26 | function get(url, lng, lat) { 27 | return wr.url(url) 28 | .query(conf) 29 | .query({ location: `${lng},${lat}` }) 30 | .get() 31 | .json(); 32 | } 33 | 34 | export function weather(lng, lat) { 35 | return get('/v7/weather/now', lng, lat); 36 | } 37 | 38 | export function minutely(lng, lat) { 39 | return get('/v7/minutely/5m', lng, lat); 40 | } 41 | 42 | export function warning(lng, lat) { 43 | return get('/v7/warning/now', lng, lat); 44 | } 45 | -------------------------------------------------------------------------------- /src/api/load-script.js: -------------------------------------------------------------------------------- 1 | import { h } from '@/util/create-element'; 2 | 3 | const promises = {}; 4 | 5 | /** 6 | * load script src, optionally run initFn() and return variable 7 | * @param {string} src 8 | * @param {string} variable 9 | * @param {string | Function} initFn 10 | * @returns {Promise} 11 | */ 12 | export function loadScript(src, variable, initFn) { 13 | if (window[variable]) return Promise.resolve(window[variable]); 14 | if (promises[variable]) return promises[variable]; 15 | const script = h('script', { src }); 16 | promises[variable] = new Promise((resolve, reject) => { 17 | script.onload = () => { 18 | if (typeof initFn == 'function') initFn(window[variable]); 19 | else if (typeof window[initFn] === 'function') window[initFn](); 20 | resolve(window[variable]); 21 | }; 22 | script.onerror = () => { 23 | reject(); 24 | delete promises[variable]; 25 | document.head.removeChild(script); 26 | }; 27 | document.head.appendChild(script); 28 | }); 29 | return promises[variable]; 30 | } 31 | -------------------------------------------------------------------------------- /src/api/plan-runnable.js: -------------------------------------------------------------------------------- 1 | const Level = { 2 | Success: 'success', 3 | Primary: 'primary', 4 | Warning: 'warning', 5 | Danger: 'danger', 6 | Error: 'error' 7 | }; 8 | 9 | /** 10 | * @param {ApiTypes.CaiYunRealtime} data 11 | */ 12 | export function forecastLevel(data) { 13 | const { 14 | wind: { speed: wind_speed }, // unit: m/s 15 | precipitation: { 16 | local: { intensity: precipitation_intensity }, 17 | nearest: { distance: precipitation_distance } = { distance: 65535 } 18 | } 19 | } = data.result; 20 | let level; 21 | if (wind_speed >= 10 || precipitation_intensity >= 0.4 || precipitation_distance < 0) { 22 | level = Level.Error; 23 | } else if (wind_speed >= 6 || precipitation_intensity >= 0.3 || precipitation_distance < 1) { 24 | level = Level.Danger; 25 | } else if (wind_speed >= 4 || precipitation_intensity >= 0.1 || precipitation_distance < 2) { 26 | level = Level.Warning; 27 | } else if (wind_speed >= 2 || precipitation_intensity > 0 || precipitation_distance < 3) { 28 | level = Level.Primary; 29 | } else { 30 | level = Level.Success; 31 | } 32 | return { 33 | level, 34 | wind: { 35 | speed: wind_speed 36 | }, 37 | rain: { 38 | distance: precipitation_distance, 39 | intensity: precipitation_intensity 40 | } 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/api/sdwc.js: -------------------------------------------------------------------------------- 1 | import wretch from 'wretch'; 2 | 3 | let wr = wretch(); 4 | 5 | export function setBaseURL(url = '') { 6 | wr = wr.url(url.replace(/\/$/, ''), true); 7 | } 8 | 9 | let confProm; 10 | /** @returns {Promise} */ 11 | export function config() { 12 | if (confProm) return confProm; 13 | return confProm = wr.url('/config.json') 14 | .get() 15 | .json(); 16 | } 17 | -------------------------------------------------------------------------------- /src/api/super-dock.js: -------------------------------------------------------------------------------- 1 | import wretch from 'wretch'; 2 | import formData from 'wretch/addons/formData'; 3 | 4 | let wr = wretch().addon(formData); 5 | 6 | export function setBaseURL(url = '') { 7 | wr = wr.url(url.replace(/\/$/, ''), true); 8 | } 9 | 10 | export function setAuth(token = '') { 11 | wr = wr.auth(token); 12 | } 13 | 14 | /** @returns {Promise} */ 15 | export function token(username, password, client_id, client_secret) { 16 | return wr.url('/oauth/token') 17 | .post({ 18 | username, 19 | password, 20 | client_id, 21 | client_secret, 22 | grant_type: 'password' 23 | }) 24 | .json(); 25 | } 26 | 27 | export function logout(token) { 28 | return wr.url('/oauth/revoke') 29 | .post({ token }) 30 | .json(); 31 | } 32 | 33 | export function user() { 34 | return wr.url('/api/v1/user/') 35 | .get() 36 | .json(); 37 | } 38 | 39 | export const DefaultSidebar = [ 40 | { type: 'overview' }, 41 | { type: 'plan' }, 42 | { type: 'node', args: 'drone' }, 43 | { type: 'node', args: 'depot' } 44 | ]; 45 | 46 | /** @returns {Promise} */ 47 | export function sidebar() { 48 | return wr.url('/api/v1/sidebar/') 49 | .get() 50 | .json() 51 | .catch(() => DefaultSidebar); 52 | } 53 | 54 | /** @returns {Promise} */ 55 | export function nodes() { 56 | return wr.url('/api/v1/nodes/') 57 | .get() 58 | .json(json => json || []); 59 | } 60 | 61 | /** @returns {Promise} */ 62 | export function plans() { 63 | return wr.url('/api/v2/plans/') 64 | .get() 65 | .json(json => json || []); 66 | } 67 | 68 | /** 69 | * @param {SDWC.PlanInfo} 70 | * @returns {Promise} 71 | */ 72 | export function createPlan(plan) { 73 | return wr.url('/api/v2/plans/') 74 | .post(plan) 75 | .json(); 76 | } 77 | 78 | /** 79 | * @param {number} id Plan ID 80 | * @param {SDWC.PlanInfo} plan PlanInfo 81 | * @returns {Promise} 82 | */ 83 | export function updatePlan(id, plan) { 84 | return wr.url(`/api/v2/plans/${id}`) 85 | .put(plan) 86 | .json(); 87 | } 88 | 89 | export function deletePlan(id) { 90 | return wr.url(`/api/v2/plans/${id}`) 91 | .delete() 92 | .json(); 93 | } 94 | 95 | export function runPlanJob(id) { 96 | return wr.url(`/api/v2/plans/${id}/jobs/`) 97 | .post() 98 | .text(); 99 | } 100 | 101 | /** @returns {Promise} */ 102 | export function getPlanJobs(id) { 103 | return wr.url(`/api/v2/plans/${id}/jobs/`) 104 | .get() 105 | .json(json => json || []); 106 | } 107 | 108 | export function cancelPlanJob(planId, jobId) { 109 | return wr.url(`/api/v2/plans/${planId}/jobs/${jobId}/cancel`) 110 | .post() 111 | .text(); 112 | } 113 | 114 | export function downloadFile(url) { 115 | return wr.url(url) 116 | .get() 117 | .res(); 118 | } 119 | 120 | export function downloadBlob(id) { 121 | return downloadFile(`/api/v2/blobs/${id}`); 122 | } 123 | 124 | /** 125 | * @param {{ [key: string]: File }} files 126 | * @returns {Promise<{ [key: string]: string }>} 127 | */ 128 | export function uploadFile(files) { 129 | return wr.url('/api/v2/blobs/') 130 | .formData(files) 131 | .post() 132 | .json(); 133 | } 134 | -------------------------------------------------------------------------------- /src/api/wretch-jsonp.js: -------------------------------------------------------------------------------- 1 | import wretch from 'wretch'; 2 | import fetchJSONP from 'fetch-jsonp'; 3 | 4 | /** 5 | * Creating a middleware instead of using polyfills: 6 | * @see https://github.com/elbywan/wretch/issues/17#issuecomment-369852335 7 | * @type {import('wretch').ConfiguredMiddleware} 8 | */ 9 | const jsonpMiddleware = () => (url, options) => { 10 | // Return your own fetch polyfill instead of calling next 11 | // (next is the global fetch here since this middleware is the terminating one) 12 | return fetchJSONP(url, options); 13 | }; 14 | 15 | export default wretch().middlewares([jsonpMiddleware]); 16 | -------------------------------------------------------------------------------- /src/components/card.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 49 | 50 | 71 | -------------------------------------------------------------------------------- /src/components/countdown-button.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 66 | -------------------------------------------------------------------------------- /src/components/custom/custom-item.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 29 | -------------------------------------------------------------------------------- /src/components/custom/custom.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 54 | 55 | 74 | -------------------------------------------------------------------------------- /src/components/job-file/iframe-doc.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 31 | -------------------------------------------------------------------------------- /src/components/job-file/iframe.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 31 | 32 | 40 | -------------------------------------------------------------------------------- /src/components/job-file/image.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | 33 | 40 | -------------------------------------------------------------------------------- /src/components/job-file/job-file.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 87 | 88 | 105 | -------------------------------------------------------------------------------- /src/components/job-file/photosphere.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 48 | 49 | 55 | -------------------------------------------------------------------------------- /src/components/job-file/text.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 29 | 30 | 36 | -------------------------------------------------------------------------------- /src/components/job-file/unknown.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 56 | 57 | 82 | -------------------------------------------------------------------------------- /src/components/job-file/url.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 50 | 51 | 59 | -------------------------------------------------------------------------------- /src/components/map/common.js: -------------------------------------------------------------------------------- 1 | export const Colors = [ 2 | '#409EFF', // element primary blue 3 | '#67C23A', // element success green 4 | '#E6A23C', // element warning orange 5 | '#F56C6C', // element danger red 6 | '#909399', // element info grey 7 | '#f69730', // amap-ui default orange 8 | '#8adaff', // amap-ui default lightblue 9 | '#bbf970', // amap-ui default lightgreen, 10 | '#ff8e7f', // amap-ui default salmon 11 | '#d252b9', // amap-ui default orchid 12 | ]; 13 | 14 | export function randColor(name) { 15 | return Colors[Number.parseInt(name, 36) % Colors.length]; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/map/map.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 34 | 35 | 50 | -------------------------------------------------------------------------------- /src/components/map/mapbox-loader.js: -------------------------------------------------------------------------------- 1 | const Mapbox = { 2 | /** @type {import('mapbox-gl')} */ 3 | js: null, 4 | /** @type {import('webpack').Module} */ 5 | style: null 6 | }; 7 | 8 | export function loadMapbox() { 9 | if (!Mapbox.style) { 10 | Mapbox.style = import(/* webpackChunkName: 'mapbox-gl-style' */ 'mapbox-gl/dist/mapbox-gl.css'); 11 | } 12 | if (!Mapbox.js) { 13 | Mapbox.js = import(/* webpackChunkName: 'mapbox-gl' */ 'mapbox-gl'); 14 | } 15 | return Mapbox.js; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/monitor/flv.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 53 | -------------------------------------------------------------------------------- /src/components/monitor/hls.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 39 | -------------------------------------------------------------------------------- /src/components/monitor/iframe.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | -------------------------------------------------------------------------------- /src/components/monitor/img.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | -------------------------------------------------------------------------------- /src/components/monitor/webrtc-base.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 58 | 59 | 71 | -------------------------------------------------------------------------------- /src/components/monitor/webrtc-srs.vue: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /src/components/monitor/webrtc.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 69 | -------------------------------------------------------------------------------- /src/components/monitor/webrtc2-client.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { EventEmitter } from 'eventemitter3'; 4 | 5 | import { trace } from './webrtc-client'; 6 | 7 | export class WebRTC2Client extends EventEmitter { 8 | /** 9 | * @param {string | RTCIceServer[]} iceServer 10 | */ 11 | constructor(iceServer) { 12 | super(); 13 | /** @type {RTCConfiguration} */ 14 | const configuration = { 15 | iceServers: typeof iceServer === 'string' ? [{ urls: iceServer }] : iceServer 16 | }; 17 | const pc = new RTCPeerConnection(configuration); 18 | this.pc = pc; 19 | pc.addEventListener('iceconnectionstatechange', () => this.onIceConnStateChange()); 20 | pc.addEventListener('icecandidate', ev => this.onIceCandidate(ev)); 21 | pc.addEventListener('track', ev => this.onTrack(ev)); 22 | pc.addTransceiver('video', { direction: 'sendrecv' }); 23 | pc.createOffer().then(offer => { 24 | trace('createOffer', offer); 25 | pc.setLocalDescription(offer); 26 | }).catch(e => trace('ERROR: createOffer', e)); 27 | } 28 | 29 | onIceConnStateChange() { 30 | trace('IceConnStateChange', this.pc.iceConnectionState); 31 | this.emit('icestatechange', this.pc.iceConnectionState); 32 | } 33 | 34 | /** 35 | * @param {RTCPeerConnectionIceEvent} ev 36 | */ 37 | onIceCandidate(ev) { 38 | // Use `turn` need this 39 | trace('IceCandidate', ev.candidate); 40 | if (ev.candidate === null) { 41 | this.emit('candidatecomplete'); 42 | } 43 | } 44 | 45 | /** 46 | * @param {RTCTrackEvent} ev 47 | */ 48 | onTrack(ev) { 49 | trace('Track', ev.streams); 50 | this.emit('track', ev.streams); 51 | } 52 | 53 | /** 54 | * @param {RTCSessionDescriptionInit} remoteSdp 55 | */ 56 | startSession(remoteSdp) { 57 | return this.pc.setRemoteDescription(remoteSdp); 58 | } 59 | 60 | destroy() { 61 | this.pc.close(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/monitor/webrtc2.vue: -------------------------------------------------------------------------------- 1 | 57 | -------------------------------------------------------------------------------- /src/components/monitor/webrtc3-client.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { EventEmitter } from 'eventemitter3'; 4 | 5 | import { trace } from './webrtc-client'; 6 | 7 | export class WebRTC3Client extends EventEmitter { 8 | /** 9 | * @param {string | RTCIceServer[]} iceServer 10 | * @param {string} cableURL 11 | */ 12 | constructor(iceServer, cableURL) { 13 | super(); 14 | const cable = new WebSocket(cableURL); 15 | this.cable = cable; 16 | cable.addEventListener('open', () => this.onCableOpen()); 17 | cable.addEventListener('close', () => this.onCableClose()); 18 | cable.addEventListener('message', e => this.onCableMessage(e)); 19 | const pc = new RTCPeerConnection({ iceServers: typeof iceServer === 'string' ? [{ urls: iceServer }] : iceServer }); 20 | this.pc = pc; 21 | pc.addEventListener('iceconnectionstatechange', () => this.onIceConnStateChange()); 22 | pc.addEventListener('icecandidate', e => this.onIceCandidate(e)); 23 | pc.addEventListener('track', e => this.onTrack(e)); 24 | } 25 | 26 | /** 27 | * @param {any} data 28 | */ 29 | cableSend(data) { 30 | trace('cableSend', data); 31 | this.cable.send(typeof data === 'string' ? data : JSON.stringify(data)); 32 | } 33 | 34 | onCableOpen() { 35 | trace('onCableOpen'); 36 | this.cableSend('SESSION_OK'); 37 | } 38 | 39 | onCableClose() { 40 | trace('onCableClose'); 41 | } 42 | 43 | /** 44 | * @param {MessageEvent} event 45 | */ 46 | onCableMessage(event) { 47 | trace('onCableMessage', event.data); 48 | try { 49 | const msg = JSON.parse(event.data); 50 | if (msg.sdp) { 51 | this.onRemoteSdp(msg.sdp); 52 | } else if (msg.ice) { 53 | this.onRemoteIceCandidate(msg.ice); 54 | } 55 | } catch (e) { /* noop */ } 56 | } 57 | 58 | /** 59 | * @param {RTCSessionDescriptionInit} sdp 60 | */ 61 | async onRemoteSdp(sdp) { 62 | trace('onRemoteSdp', sdp); 63 | this.pc.addTransceiver('video', { direction: 'recvonly' }); 64 | await this.pc.setRemoteDescription(sdp); 65 | const answer = await this.pc.createAnswer(); 66 | this.pc.setLocalDescription(answer); 67 | this.cableSend({ sdp: answer }); 68 | } 69 | 70 | /** 71 | * @param {RTCIceCandidateInit} ice 72 | */ 73 | onRemoteIceCandidate(ice) { 74 | trace('onRemoteCandidate', ice); 75 | this.pc.addIceCandidate(ice); 76 | } 77 | 78 | onIceConnStateChange() { 79 | trace('onIceConnStateChange', this.pc.iceConnectionState); 80 | this.emit('icestatechange', this.pc.iceConnectionState); 81 | } 82 | 83 | /** 84 | * @param {RTCPeerConnectionIceEvent} ev 85 | */ 86 | onIceCandidate(ev) { 87 | // Use `turn` need this 88 | trace('onIceCandidate', ev.candidate); 89 | if (ev.candidate) { 90 | this.cableSend({ ice: ev.candidate }); 91 | } 92 | } 93 | 94 | /** 95 | * @param {RTCTrackEvent} ev 96 | */ 97 | onTrack(ev) { 98 | trace('onTrack', ev.streams); 99 | this.emit('track', ev.streams); 100 | } 101 | 102 | destroy() { 103 | this.cable.close(); 104 | this.pc.close(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/components/monitor/webrtc3.vue: -------------------------------------------------------------------------------- 1 | 53 | -------------------------------------------------------------------------------- /src/components/monitor/webrtc4.vue: -------------------------------------------------------------------------------- 1 | 50 | -------------------------------------------------------------------------------- /src/components/plan-dialog/plan-dialog-item.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 53 | 54 | 86 | -------------------------------------------------------------------------------- /src/components/plan-dialog/plan-dialog.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 101 | 102 | 114 | -------------------------------------------------------------------------------- /src/components/plan/editable.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 69 | 70 | 84 | -------------------------------------------------------------------------------- /src/components/plan/readonly.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 65 | 66 | 76 | -------------------------------------------------------------------------------- /src/components/schedule/schedule-edit-advanced.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 37 | 38 | 40 | -------------------------------------------------------------------------------- /src/components/schedule/schedule-edit-simple.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 55 | 56 | 68 | -------------------------------------------------------------------------------- /src/components/sd-icon.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | -------------------------------------------------------------------------------- /src/components/settings/settings-input.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 53 | 54 | 62 | -------------------------------------------------------------------------------- /src/components/settings/settings-radio.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 47 | -------------------------------------------------------------------------------- /src/components/settings/settings-slider.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 75 | 76 | 133 | -------------------------------------------------------------------------------- /src/components/settings/settings-switch.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 29 | -------------------------------------------------------------------------------- /src/components/status/drone-status.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 115 | -------------------------------------------------------------------------------- /src/components/status/status-notify-item.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 33 | 34 | 90 | -------------------------------------------------------------------------------- /src/components/status/status.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 79 | 80 | 97 | -------------------------------------------------------------------------------- /src/components/weather/alert-badge.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 60 | 61 | 106 | -------------------------------------------------------------------------------- /src/components/weather/rain-chart.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 94 | 95 | 116 | -------------------------------------------------------------------------------- /src/components/weather/wind-icon.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 86 | 87 | 97 | -------------------------------------------------------------------------------- /src/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "beian": "", 3 | "title": "S Dashboard Web Console", 4 | "aside_logo": "https://sb.im/img/sb-logo.png", 5 | "super_dock_api_server": "https://", 6 | "oauth_client_id": "", 7 | "oauth_client_secret": "", 8 | "lang": "en", 9 | "ice_server": "stun:stun.l.google.com:19302", 10 | "ice_servers": [ 11 | { 12 | "urls": [ 13 | "stun:stun.l.google.com:19302" 14 | ], 15 | "username": "", 16 | "credential": "" 17 | } 18 | ], 19 | "map_tiles_url": "", 20 | "caiyun_key": "", 21 | "heweather_key": "", 22 | "mqtt_url": "wss://:/" 23 | } 24 | -------------------------------------------------------------------------------- /src/constants/browser.js: -------------------------------------------------------------------------------- 1 | import Bowser from 'bowser'; 2 | 3 | const result = Bowser.parse(navigator.userAgent); 4 | 5 | if (process.env.NODE_ENV === 'development') { // eslint-disable-line no-undef 6 | console.log('Browser and platform detection', result); // eslint-disable-line no-console 7 | } 8 | 9 | export default result; 10 | export const isSafari = result.browser.name === 'Safari'; 11 | export const isiPad = result.platform.model === 'iPad'; 12 | -------------------------------------------------------------------------------- /src/constants/drone-place-style.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {{ [key: string]: SDWC.DronePlaceStyle }} 3 | */ 4 | export const PlaceStyle = { 5 | target: { stroke: 'dotted', color: '#409eff' }, 6 | roi: { point: 'glow', color: '#f69730' }, 7 | home: { color: '#67c23a' } 8 | }; 9 | -------------------------------------------------------------------------------- /src/constants/node-notification-levels.js: -------------------------------------------------------------------------------- 1 | export const NodeNotificationLevels = [ 2 | { name: 'Emergency', class: 'lv0 el-icon-message-solid' }, 3 | { name: 'Alert', class: 'lv1 el-icon-warning' }, 4 | { name: 'Critical', class: 'lv2 el-icon-error' }, 5 | { name: 'Error', class: 'lv3 el-icon-remove' }, 6 | { name: 'Warn', class: 'lv4 el-icon-warning' }, 7 | { name: 'Notice', class: 'lv5 el-icon-circle-plus' }, 8 | { name: 'Info', class: 'lv6 el-icon-info' }, 9 | { name: 'Debug', class: 'lv7 el-icon-s-tools' } 10 | ]; 11 | -------------------------------------------------------------------------------- /src/constants/node-status-class.js: -------------------------------------------------------------------------------- 1 | export const NodeStatusClass = { 2 | // front-end only: initial value in store, haven't connected 3 | '-1': 'el-icon-warning color--orange', 4 | // normal 5 | 0: 'el-icon-success color--green', 6 | // powered off 7 | 1: 'el-icon-info color--gery', 8 | // network error 9 | 2: 'el-icon-error color--red' 10 | }; 11 | -------------------------------------------------------------------------------- /src/constants/plan-dialog-level-class.js: -------------------------------------------------------------------------------- 1 | export const PlanDialogLevelClass = { 2 | success: 'el-icon-success color--green', 3 | primary: 'el-icon-circle-plus color--blue', 4 | info: 'el-icon-info color-gery', 5 | warning: 'el-icon-warning color--orange', 6 | danger: 'el-icon-error color--red', 7 | unknown: 'el-icon-question color--grey' 8 | }; 9 | -------------------------------------------------------------------------------- /src/constants/rpc-status-class.js: -------------------------------------------------------------------------------- 1 | export const RpcStatusClass = { 2 | // success 3 | 0: 'el-icon-success color--green', 4 | // pending 5 | 1: 'el-icon-question color--grey', 6 | // error 7 | 2: 'el-icon-error color--red', 8 | // notification 9 | 3: 'el-icon-info color--blue', 10 | default: 'el-icon-warning color--orange' 11 | }; 12 | -------------------------------------------------------------------------------- /src/i18n/common.js: -------------------------------------------------------------------------------- 1 | /** @type {{ [key: string]: Intl.DateTimeFormatOptions }} */ 2 | export const format = { 3 | // YYYY-MM-DD HH:mm 4 | long: { 5 | year: 'numeric', 6 | month: '2-digit', 7 | day: '2-digit', 8 | hour: '2-digit', 9 | minute: '2-digit', 10 | hourCycle: 'h23' 11 | }, 12 | // HH:mm 13 | time: { 14 | hour: '2-digit', 15 | minute: '2-digit', 16 | hourCycle: 'h23' 17 | }, 18 | // HH:mm:ss 19 | seconds: { 20 | hour: '2-digit', 21 | minute: '2-digit', 22 | second: '2-digit', 23 | hourCycle: 'h23' 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import I18n from 'vue-i18n'; 3 | 4 | import { format } from './common'; 5 | import * as zh from './locale/zh'; 6 | import * as en from './locale/en'; 7 | 8 | Vue.use(I18n); 9 | 10 | const i18n = new I18n({ 11 | locale: navigator.language.split('-')[0], 12 | messages: { 13 | 'en': en.default, 14 | 'zh': zh.default 15 | }, 16 | dateTimeFormats: { 17 | 'en': format, 18 | 'zh': format 19 | }, 20 | silentTranslationWarn: process.env.NODE_ENV !== 'development' // eslint-disable-line no-undef 21 | }); 22 | 23 | export default i18n; 24 | 25 | export const locales = { 26 | en: en.name, 27 | zh: zh.name 28 | }; 29 | 30 | export function setLocale(locale) { 31 | i18n.locale = locale; 32 | document.body.lang = locale; 33 | } 34 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import { mqtt } from '@/util/plugin-mqtt' 4 | 5 | declare module 'vue/types/vue' { 6 | interface Vue { 7 | $mqtt: typeof mqtt; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/embedded.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 51 | 52 | 88 | -------------------------------------------------------------------------------- /src/pages/iframe.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 27 | -------------------------------------------------------------------------------- /src/pages/overview/overview-notify.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 32 | 33 | 40 | -------------------------------------------------------------------------------- /src/pages/overview/overview-popover.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 81 | 82 | 133 | -------------------------------------------------------------------------------- /src/pages/overview/overview.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 32 | -------------------------------------------------------------------------------- /src/pages/panel/panel.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | 24 | 57 | -------------------------------------------------------------------------------- /src/pages/panel/team-dialog.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 73 | -------------------------------------------------------------------------------- /src/pages/plan/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {SDWC.ParsedWaypoint} waypoint 3 | * @returns {{ boundary: SDWC.LatLng[], polylines: SDWC.MapPolyline[], markers: SDWC.MarkerAction[] }} 4 | */ 5 | export function waypointsToMapProps(waypoint) { 6 | return { 7 | boundary: waypoint.boundary || [], 8 | polylines: [{ 9 | name: 'path', 10 | style: { stroke: 'solid', color: '#ea4335' }, 11 | coordinates: waypoint.path 12 | }], 13 | markers: waypoint.actions 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/plan/edit.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 92 | -------------------------------------------------------------------------------- /src/pages/plan/new.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 74 | -------------------------------------------------------------------------------- /src/pages/plan/plan.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 42 | -------------------------------------------------------------------------------- /src/pages/schedule/edit.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 75 | 76 | 78 | -------------------------------------------------------------------------------- /src/pages/schedule/new.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 64 | 65 | 67 | -------------------------------------------------------------------------------- /src/pages/schedule/schedule.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 43 | -------------------------------------------------------------------------------- /src/pages/schedule/view.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 60 | 61 | 63 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {SDWC.State} state 5 | * @returns {boolean} 6 | */ 7 | export function authenticated(state) { 8 | const { implicit, token, expire } = state.user.credential; 9 | return implicit || (token.length > 0 && new Date(expire).getTime() > Date.now()); 10 | } 11 | 12 | /** 13 | * @param {SDWC.State} state 14 | * @returns {SDWC.Node[]} 15 | */ 16 | export function depots(state) { 17 | return state.node.filter(n => n.status.status.type === 'depot'); 18 | } 19 | 20 | /** 21 | * @param {SDWC.State} state 22 | * @returns {SDWC.Node[]} 23 | */ 24 | export function drones(state) { 25 | return state.node.filter(n => n.status.status.type === 'drone'); 26 | } 27 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | import * as modules from './modules'; 5 | import * as actions from './actions'; 6 | import * as getters from './getters'; 7 | 8 | Vue.use(Vuex); 9 | 10 | /** 11 | * @type {import('vuex').Store} 12 | */ 13 | export default new Vuex.Store({ 14 | modules, 15 | actions, 16 | getters 17 | }); 18 | -------------------------------------------------------------------------------- /src/store/modules/config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {SDWC.Config} */ 4 | const state = { 5 | beian: '', 6 | title: 'S Dashboard Web Console', 7 | aside_logo: '', 8 | super_dock_api_server: '', 9 | lang: 'en', 10 | ice_server: '', 11 | ice_servers: null, 12 | map_tiles_url: '', 13 | caiyun_key: '', 14 | heweather_key: '', 15 | mqtt_url: '', 16 | idle_timeout: 600 17 | }; 18 | 19 | export const MutationTypes = { 20 | SET_CONFIG: 'SET_CONFIG' 21 | }; 22 | 23 | /** 24 | * @type {{ [x: string]: (state: SDWC.Config, payload: any) => void }} 25 | */ 26 | const mutations = { 27 | [MutationTypes.SET_CONFIG](state, /** @type {Partial} */ payload) { 28 | Object.assign(state, payload); 29 | } 30 | }; 31 | 32 | export default { 33 | state, 34 | mutations 35 | }; 36 | -------------------------------------------------------------------------------- /src/store/modules/index.js: -------------------------------------------------------------------------------- 1 | export { default as notification } from './notification'; 2 | export { default as preference } from './preference'; 3 | export { default as schedule } from './schedule'; 4 | export { default as config } from './config'; 5 | export { default as user } from './user'; 6 | export { default as node } from './node'; 7 | export { default as plan } from './plan'; 8 | export { default as ui } from './ui'; 9 | -------------------------------------------------------------------------------- /src/store/modules/notification.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @type {SDWC.NotificationItem[]} 5 | */ 6 | const state = []; 7 | 8 | export const MutationTypes = { 9 | ADD_NOTI: 'ADD_NOTI', 10 | MOD_NOTI: 'MOD_NOTI', 11 | CLEAR_NOTI: 'CLEAR_NOTI' 12 | }; 13 | 14 | /** 15 | * @type {{ [x: string]: (state: SDWC.NotificationItem[], payload: any) => void }} 16 | */ 17 | const mutations = { 18 | [MutationTypes.ADD_NOTI](state, /** @type {SDWC.NotificationItem} */ payload) { 19 | state.unshift(payload); 20 | }, 21 | [MutationTypes.MOD_NOTI](state, /** @type {SDWC.NotificationItem} */ payload) { 22 | const n = state.find(i => i.id === payload.id); 23 | if (n) { 24 | n.status = payload.status; 25 | } 26 | }, 27 | [MutationTypes.CLEAR_NOTI](state) { 28 | state.splice(0); 29 | } 30 | }; 31 | 32 | export default { 33 | state, 34 | mutations 35 | }; 36 | -------------------------------------------------------------------------------- /src/store/modules/plan.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {SDWC.PlanState[]} */ 4 | const state = []; 5 | 6 | export const MutationTypes = { 7 | ADD_PLAN: 'ADD_PLAN', 8 | ADD_PLAN_TERM: 'ADD_PLAN_TERM', 9 | SET_PLAN_DIALOG: 'SET_PLAN_DIALOG', 10 | SET_PLAN_RUNNING: 'SET_PLAN_RUNNING', 11 | ADD_PLAN_NOTIFY: 'ADD_PLAN_NOTIFY', 12 | UPDATE_PLAN: 'UPDATE_PLAN', 13 | DELETE_PLAN: 'DELETE_PLAN', 14 | CLEAR_PLANS: 'CLEAR_PLANS' 15 | }; 16 | 17 | /** 18 | * @type {{ [x: string]: (state: SDWC.PlanState[], payload: any) => void }} 19 | */ 20 | const mutations = { 21 | [MutationTypes.ADD_PLAN](state, /** @type {SDWC.PlanInfo} */ payload) { 22 | if (state.findIndex(plan => plan.info.id === payload.id) >= 0) return; 23 | state.push({ 24 | info: payload, 25 | term: [], 26 | dialog: null, 27 | running: null, 28 | notification: [] 29 | }); 30 | }, 31 | [MutationTypes.ADD_PLAN_TERM](state, /** @type {{ id: number, output: string }} */ payload) { 32 | const plan = state.find(p => p.info.id === payload.id); 33 | if (!plan) return; 34 | plan.term.unshift({ time: Date.now() / 1000, msg: payload.output }); 35 | }, 36 | [MutationTypes.SET_PLAN_DIALOG](state, /** @type {{ id: number, dialog?: SDWC.PlanDialogContent }} */ payload) { 37 | const plan = state.find(p => p.info.id === payload.id); 38 | if (!plan) return; 39 | const empty = Object.getOwnPropertyNames(payload.dialog).length === 0; 40 | plan.dialog = empty ? null : Object.assign({ time: Date.now() }, payload.dialog); 41 | }, 42 | [MutationTypes.SET_PLAN_RUNNING](state, /** @type {{ id: number, running: SDWC.RunningTask }} */ payload) { 43 | const plan = state.find(p => p.info.id === payload.id); 44 | if (!plan) return; 45 | const empty = Object.getOwnPropertyNames(payload.running).length === 0; 46 | plan.running = empty ? null : payload.running; 47 | }, 48 | [MutationTypes.UPDATE_PLAN](state, /** @type {SDWC.PlanInfo} */ payload) { 49 | const plan = state.find(p => p.info.id === payload.id); 50 | if (!plan) return; 51 | plan.info = payload; 52 | }, 53 | [MutationTypes.DELETE_PLAN](state, /** @type {number} */ id) { 54 | const index = state.findIndex(p => p.info.id === id); 55 | if (index < 0) return; 56 | state.splice(index, 1); 57 | }, 58 | [MutationTypes.CLEAR_PLANS](state) { 59 | state.splice(0); 60 | } 61 | }; 62 | 63 | export default { 64 | state, 65 | mutations 66 | }; 67 | -------------------------------------------------------------------------------- /src/store/modules/preference.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {SDWC.Preference} */ 4 | const state = { 5 | lang: '', 6 | mapFollow: true, 7 | overviewFit: true, 8 | notifyNoPopup: [], 9 | rpcMsgPopup: false, 10 | planDialogPopup: true 11 | }; 12 | 13 | export const MutationTypes = { 14 | SET_PREFERENCE: 'SET_PREFERENCE' 15 | }; 16 | 17 | /** 18 | * @type {{ [x: string]: (state: SDWC.Preference, payload: any) => void }} 19 | */ 20 | const mutations = { 21 | [MutationTypes.SET_PREFERENCE](state, /** @type {Partial} */ payload) { 22 | for (const [key, value] of Object.entries(payload)) { 23 | if (Object.prototype.hasOwnProperty.call(state, key)) { 24 | state[key] = value; 25 | } 26 | } 27 | } 28 | }; 29 | 30 | export default { 31 | state, 32 | mutations 33 | }; 34 | -------------------------------------------------------------------------------- /src/store/modules/schedule.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @typedef {ApiTypes.V3.Schedule} Schedule 5 | */ 6 | 7 | /** @type {Schedule[]} */ 8 | const state = []; 9 | 10 | export const MutationTypes = { 11 | ADD_SCHEDULE: 'ADD_SCHEDULE', 12 | UPDATE_SCHEDULE: 'UPDATE_SCHEDULE', 13 | DELETE_SCHEDULE: 'DELETE_SCHEDULE', 14 | CLEAR_SCHEDULES: 'CLEAR_SCHEDULES' 15 | }; 16 | 17 | /** 18 | * @type {{ [x: string]: (state: Schedule[], payload: any) => void }} 19 | */ 20 | const mutations = { 21 | [MutationTypes.ADD_SCHEDULE](state, /** @type {Schedule} */ payload) { 22 | if (state.find(s => s.id === payload.id)) return; 23 | state.push(payload); 24 | }, 25 | [MutationTypes.UPDATE_SCHEDULE](state, /** @type {Schedule} */ payload) { 26 | const index = state.findIndex(p => p.id === payload.id); 27 | if (index < 0) return; 28 | state.splice(index, 1, payload); 29 | }, 30 | [MutationTypes.DELETE_SCHEDULE](state, /** @type {number} */ id) { 31 | const index = state.findIndex(p => p.id === id); 32 | if (index < 0) return; 33 | state.splice(index, 1); 34 | }, 35 | [MutationTypes.CLEAR_SCHEDULES](state) { 36 | state.splice(0); 37 | } 38 | }; 39 | 40 | export default { 41 | state, 42 | mutations 43 | }; 44 | -------------------------------------------------------------------------------- /src/store/modules/ui.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @type {SDWC.UI} 5 | */ 6 | const state = { 7 | idle: false, 8 | expireTimer: -1, 9 | mqttConnected: false, 10 | mqttDelay: -1, 11 | sidebar: [] 12 | }; 13 | 14 | export const MutationTypes = { 15 | SET_UI: 'SET_UI' 16 | }; 17 | 18 | /** 19 | * @type {{ [x: string]: (state: SDWC.UI, payload: any) => void }} 20 | */ 21 | const mutations = { 22 | [MutationTypes.SET_UI](state, /** @type {Partial} */ payload) { 23 | Object.assign(state, payload); 24 | } 25 | }; 26 | 27 | export default { 28 | state, 29 | mutations 30 | }; 31 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {SDWC.User} */ 4 | const state = { 5 | info: { 6 | id: -1, 7 | language: '', 8 | team_id: -1, 9 | teams: [], 10 | timezone: '', 11 | username: '', 12 | created_at: '', 13 | updated_at: '' 14 | }, 15 | credential: { 16 | implicit: false, 17 | token: '', 18 | expire: '' 19 | } 20 | }; 21 | 22 | export const MutationTypes = { 23 | SET_USER_TOKEN: 'SET_USER_TOKEN', 24 | SET_USER_INFO: 'SET_USER_INFO', 25 | INVALIDATE_TOKEN: 'INVALIDATE_TOKEN' 26 | }; 27 | 28 | /** 29 | * @type {{ [x: string]: (state: SDWC.User, payload: any) => void }} 30 | */ 31 | const mutations = { 32 | [MutationTypes.SET_USER_TOKEN](state, payload) { 33 | state.credential = payload; 34 | }, 35 | [MutationTypes.SET_USER_INFO](state, payload) { 36 | state.info = payload; 37 | }, 38 | [MutationTypes.INVALIDATE_TOKEN](state) { 39 | state.credential = { 40 | implicit: false, 41 | token: '', 42 | expire: '' 43 | }; 44 | } 45 | }; 46 | 47 | export default { 48 | state, 49 | mutations 50 | }; 51 | -------------------------------------------------------------------------------- /src/styles/chartist.css: -------------------------------------------------------------------------------- 1 | /* chartist chart style */ 2 | 3 | .ct-label { 4 | white-space: nowrap; 5 | } 6 | 7 | .ct-series .ct-line { 8 | stroke-width: 2px; 9 | } 10 | 11 | .ct-series .ct-point { 12 | stroke-width: 8px; 13 | stroke: transparent; 14 | } 15 | 16 | /* chartist-plugin-tooltips tooltip style */ 17 | 18 | .chartist-tooltip { 19 | color: #606266; 20 | background: white; 21 | font: 14px inherit normal; 22 | text-align: center; 23 | white-space: nowrap; 24 | border: 1px solid #ebeef5; 25 | border-radius: 4px; 26 | box-shadow: 0 2px 12px #00000020; 27 | } 28 | 29 | .chartist-tooltip::before { 30 | margin-left: -6px; 31 | border-width: 6px; 32 | border-top-color: white; 33 | filter: drop-shadow(0 2px 12px #00000020) drop-shadow(0 1px 0 #ebeef5); 34 | } 35 | -------------------------------------------------------------------------------- /src/styles/development.css: -------------------------------------------------------------------------------- 1 | .development-ribbon { 2 | content: ''; 3 | /* webpackIgnore: true */ 4 | background-image: url('./assets/images/staging-tip.svg'); 5 | background-repeat: no-repeat; 6 | position: fixed; 7 | top: 0; 8 | left: 0; 9 | height: 120px; 10 | width: 120px; 11 | z-index: 1; 12 | pointer-events: none; 13 | } 14 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; 4 | } 5 | 6 | /* public status colors */ 7 | .color--green { 8 | color: #67c23a; 9 | } 10 | .color--blue { 11 | /* override .el-notification .el-icon-info */ 12 | color: #409eff !important; 13 | } 14 | .color--orange { 15 | color: #e6a23c; 16 | } 17 | .color--red { 18 | color: #f56c6c; 19 | } 20 | .color--grey { 21 | color: #606266; 22 | } 23 | 24 | /* Material Icons */ 25 | @font-face { 26 | font-family: 'Material Icons'; 27 | font-style: normal; 28 | font-weight: 400; 29 | font-display: block; 30 | src: url('material-design-icons-iconfont/dist/fonts/MaterialIcons-Regular.woff2') format('woff2'), 31 | url('material-design-icons-iconfont/dist/fonts/MaterialIcons-Regular.woff') format('woff'); 32 | } 33 | .material-icons { 34 | font-family: 'Material Icons'; 35 | font-weight: normal; 36 | font-style: normal; 37 | display: inline-block; 38 | line-height: 1; 39 | text-transform: none; 40 | letter-spacing: normal; 41 | word-wrap: normal; 42 | white-space: nowrap; 43 | direction: ltr; 44 | -webkit-font-smoothing: antialiased; 45 | text-rendering: optimizeLegibility; 46 | -moz-osx-font-smoothing: grayscale; 47 | font-feature-settings: 'liga'; 48 | } 49 | 50 | @media screen and (min-width: 1080px) { /* width > 1200, 部分双栏 */ 51 | /* container */ 52 | .plan, 53 | .node { 54 | display: flex; 55 | flex-wrap: wrap; 56 | } 57 | 58 | /* plan */ 59 | .plan .sd-card { 60 | flex-basis: calc(50% - 2px); 61 | } 62 | .plan .term { 63 | flex-basis: 100%; 64 | } 65 | .plan .plan__history { 66 | flex-basis: 100%; 67 | } 68 | 69 | /* drone & depot */ 70 | .status, 71 | .battery, 72 | .debug { 73 | width: 100%; 74 | } 75 | 76 | .node .monitor, 77 | .node .sd-map, 78 | .weather, 79 | .control, 80 | .speaker { 81 | width: calc(50% - 2px); 82 | } 83 | } 84 | 85 | @media screen and (min-width: 1580px) { /* width > 1580, 全部双栏 */ 86 | /* drone & depot */ 87 | .battery, 88 | .custom, 89 | .debug { 90 | width: calc(50% - 2px); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/util/browser-hacks.js: -------------------------------------------------------------------------------- 1 | import * as B from '@/constants/browser'; 2 | 3 | if (B.isSafari && B.isiPad) { 4 | document.body.classList.add('sd--safari'); 5 | } 6 | -------------------------------------------------------------------------------- /src/util/create-element.js: -------------------------------------------------------------------------------- 1 | /** 2 | * fill attributes and children 3 | * @template {Element} T 4 | * @param {T} elm 5 | * @param {Record} attrs 6 | * @param {Element[] | string} [children] 7 | * @returns {T} 8 | */ 9 | function setupElement(elm, attrs, children) { 10 | if (attrs) { 11 | for (const [name, value] of Object.entries(attrs)) { 12 | elm.setAttribute(name, value); 13 | } 14 | } 15 | if (children) { 16 | if (typeof children === 'string') { 17 | elm.append(children); 18 | } else if (Array.isArray(children)) { 19 | for (const child of children) { 20 | elm.append(child); 21 | } 22 | } 23 | } 24 | return elm; 25 | } 26 | 27 | /** 28 | * create HTML Element 29 | * @param {keyof HTMLElementTagNameMap} tag 30 | * @param {Record} attrs 31 | * @param {Element[] | string} [children] 32 | * @returns {HTMLElement} 33 | */ 34 | export function h(tag, attrs, children) { 35 | const elm = document.createElement(tag); 36 | return setupElement(elm, attrs, children); 37 | } 38 | 39 | /** 40 | * create SVG Element 41 | * @param {keyof SVGElementTagNameMap} tag 42 | * @param {Record} attrs 43 | * @param {Element[] | string} [children] 44 | * @returns {SVGElement} 45 | */ 46 | export function hs(tag, attrs, children) { 47 | const elm = document.createElementNS('http://www.w3.org/2000/svg', tag); 48 | return setupElement(elm, attrs, children); 49 | } 50 | -------------------------------------------------------------------------------- /src/util/wait-selector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {HTMLElement} root 3 | * @param {string} selector 4 | * @param {boolean} exist 5 | * @returns {Promise} 6 | */ 7 | export function waitSelector(root, selector, exist = true) { 8 | return new Promise(resolve => { 9 | const callback = () => { 10 | const q = root.querySelector(selector); 11 | return (exist === !!q) ? resolve(q) : requestAnimationFrame(callback); 12 | }; 13 | requestAnimationFrame(callback); 14 | }); 15 | } 16 | --------------------------------------------------------------------------------