first commit
29
.gitignore
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
### Go template
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
### Example user template template
|
||||||
|
### Example user template
|
||||||
|
|
||||||
|
# IntelliJ project files
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
out
|
||||||
|
gen
|
||||||
|
/logs/
|
||||||
|
/sql/
|
||||||
|
/out/
|
||||||
|
/db/
|
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "UI"]
|
||||||
|
path = UI
|
||||||
|
url = https://github.com/ZimaBoard/CasaOS-UI.git
|
13
Makefile
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.PHONY:build build-ui build-backend help
|
||||||
|
|
||||||
|
build: build-ui build-backend
|
||||||
|
|
||||||
|
|
||||||
|
build-ui:
|
||||||
|
cd UI && yarn install && yarn build
|
||||||
|
|
||||||
|
build-backend:
|
||||||
|
export CGO_ENABLED=1;export CGO_LDFLAGS=-static;go mod tidy;go build -o ./casa main.go;upx --lzma --best casa
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "call john"
|
7
README-ZH.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
- conf 配置文件
|
||||||
|
- route 路由
|
||||||
|
- service 方法的具体实现
|
||||||
|
- utils 工具
|
||||||
|
- main.go 入口
|
2
README.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# Oasis
|
||||||
|
|
3
UI/.browserslistrc
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
2
UI/.env.dev
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
// .env.dev
|
||||||
|
NODE_ENV='dev'
|
2
UI/.env.production
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
// .env.production
|
||||||
|
NODE_ENV='prod'
|
18
UI/.eslintrc.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
'extends': [
|
||||||
|
'plugin:vue/essential',
|
||||||
|
'eslint:recommended'
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
parser: 'babel-eslint'
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||||
|
'vue/no-unused-vars':'off'
|
||||||
|
}
|
||||||
|
}
|
23
UI/.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
32
UI/README.md
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<!--
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-22 14:24:43
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-22 14:44:31
|
||||||
|
* @Description:
|
||||||
|
* @FilePath: /CasaOS-UI/README.md
|
||||||
|
-->
|
||||||
|
# CasaOS-UI
|
||||||
|
|
||||||
|
The front-end of CasaOs,build with VueJS
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
yarn serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
Will be output to the ../web folder
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
yarn lint
|
||||||
|
```
|
5
UI/babel.config.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
43
UI/package.json
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"name": "CasaOS",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve --mode dev",
|
||||||
|
"build": "vue-cli-service build --no-clean --dest ../web --mode production",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.21.4",
|
||||||
|
"buefy": "^0.9.0",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"easy-affix": "^1.0.8",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"lottie-vuejs": "^0.4.0",
|
||||||
|
"moment": "^2.29.1",
|
||||||
|
"nth-check": "^2.0.1",
|
||||||
|
"qs": "^6.10.1",
|
||||||
|
"vee-validate": "^3.4.12",
|
||||||
|
"vue": "^2.6.11",
|
||||||
|
"vue-router": "^3.2.0",
|
||||||
|
"vue-slider-component": "^3.2.14",
|
||||||
|
"vuex": "^3.4.0",
|
||||||
|
"vuex-persistedstate": "^4.0.0",
|
||||||
|
"yargs-parser": "^20.2.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-router": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||||
|
"@vue/cli-service": "~4.5.0",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"compression-webpack-plugin": "^9.0.0",
|
||||||
|
"eslint": "^6.7.2",
|
||||||
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
"node-sass": "^4.9.0",
|
||||||
|
"sass-loader": "^7.0.1",
|
||||||
|
"vue-cli-plugin-buefy": "~0.3.8",
|
||||||
|
"vue-template-compiler": "^2.6.11"
|
||||||
|
}
|
||||||
|
}
|
9
UI/public/browserconfig.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/ui/img/icon/mstile-150x150.png"/>
|
||||||
|
<TileColor>#da532c</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
BIN
UI/public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
UI/public/img/ani/icon/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
UI/public/img/ani/icon/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
UI/public/img/ani/icon/favicon-16x16.png
Normal file
After Width: | Height: | Size: 693 B |
BIN
UI/public/img/ani/icon/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
UI/public/img/ani/icon/mstile-150x150.png
Normal file
After Width: | Height: | Size: 6 KiB |
25
UI/public/img/ani/icon/safari-pinned-tab.svg
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="200.000000pt" height="200.000000pt" viewBox="0 0 200.000000 200.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(0.000000,200.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M875 1894 c-11 -2 -51 -11 -88 -20 -341 -78 -610 -364 -673 -714 -82
|
||||||
|
-459 197 -902 647 -1030 94 -27 277 -37 378 -21 303 47 575 261 690 540 55
|
||||||
|
133 66 192 65 361 -1 136 -4 165 -27 235 -36 116 -62 170 -123 261 -123 186
|
||||||
|
-347 336 -566 379 -42 8 -276 15 -303 9z m250 -168 c11 -2 42 -9 70 -16 131
|
||||||
|
-30 288 -135 387 -260 160 -201 198 -506 93 -745 l-22 -50 2 80 c3 180 -64
|
||||||
|
351 -184 476 -209 216 -544 260 -807 105 -201 -117 -326 -347 -319 -587 l2
|
||||||
|
-74 -19 44 c-63 140 -80 332 -44 476 24 94 87 219 147 292 109 133 290 238
|
||||||
|
448 259 25 3 47 7 49 9 5 4 173 -3 197 -9z m8 -501 c33 -8 85 -31 116 -50 227
|
||||||
|
-137 305 -418 183 -651 l-21 -39 -1 35 c-9 251 -245 439 -490 389 -186 -38
|
||||||
|
-323 -200 -330 -389 l-1 -35 -23 45 c-101 194 -61 429 99 578 130 122 292 162
|
||||||
|
468 117z m-27 -499 c182 -85 183 -345 1 -438 -53 -27 -161 -26 -215 1 -146 75
|
||||||
|
-180 267 -69 390 63 69 190 90 283 47z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
UI/public/img/ani/rocket-launching.json
Normal file
42
UI/public/index.html
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<!--
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-22 14:24:43
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-24 18:03:02
|
||||||
|
* @Description:
|
||||||
|
* @FilePath: /CasaOS-UI/public/index.html
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="<%= BASE_URL %>img/icon/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="<%= BASE_URL %>img/icon/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="<%= BASE_URL %>img/icon/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="<%= BASE_URL %>site.webmanifest">
|
||||||
|
<link rel="mask-icon" href="<%= BASE_URL %>img/icon/safari-pinned-tab.svg" color="#5bbad5">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<link rel="stylesheet" href="//cdn.materialdesignicons.com/2.0.46/css/materialdesignicons.min.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.15.4/js/all.js"
|
||||||
|
integrity="sha256-GaerX2a/DuOnPrxn/4vH13dobiFUe/27LO6gCZDNauA=" crossorigin="anonymous"></script>
|
||||||
|
<title>
|
||||||
|
<%= htmlWebpackPlugin.options.title %>
|
||||||
|
</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
|
||||||
|
Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
14
UI/public/site.webmanifest
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"short_name": "",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/ui/img/icon/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
110
UI/src/App.vue
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
<!--
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-23 16:42:14
|
||||||
|
* @Description: Main entry of application
|
||||||
|
* @FilePath: /CasaOS-UI/src/App.vue
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- <div id="app" class="is-flex is-flex-direction-column" :style="{'background-image': 'url(' + require('./assets/background/AbstractShapes.jpg') + ')'}"> -->
|
||||||
|
<div id="app" class="is-flex is-flex-direction-column" :style="{'background-image': 'url(https://www.bing.com/th?id=OHR.Aldeyjarfoss_ZH-CN0106567013_1920x1080.jpg&rf=LaDigue_1920x1080.jpg&pid=hp)'}">
|
||||||
|
<!-- NavBar Start -->
|
||||||
|
<top-bar></top-bar>
|
||||||
|
<!-- NavBar End -->
|
||||||
|
|
||||||
|
<!-- Content Start -->
|
||||||
|
<div class="contents pt-55 pb-6">
|
||||||
|
<div class="container">
|
||||||
|
<div class="is-flex">
|
||||||
|
<!-- SideBar Start -->
|
||||||
|
<side-bar></side-bar>
|
||||||
|
<!-- SideBar End -->
|
||||||
|
|
||||||
|
<!-- MainContent Start -->
|
||||||
|
<div class="main-content">
|
||||||
|
<!-- SearchBar Start -->
|
||||||
|
<section>
|
||||||
|
<search-bar></search-bar>
|
||||||
|
</section>
|
||||||
|
<!-- SearchBar End -->
|
||||||
|
|
||||||
|
<!-- Suggestions For You Start -->
|
||||||
|
<section>
|
||||||
|
<suggestion></suggestion>
|
||||||
|
</section>
|
||||||
|
<!-- Suggestions For You End -->
|
||||||
|
|
||||||
|
<!-- Apps Start -->
|
||||||
|
<section>
|
||||||
|
<apps></apps>
|
||||||
|
</section>
|
||||||
|
<!-- Apps End -->
|
||||||
|
|
||||||
|
<!-- Shortcuts Start -->
|
||||||
|
<!-- <section>
|
||||||
|
<shortcuts></shortcuts>
|
||||||
|
</section> -->
|
||||||
|
<!-- Shortcuts End -->
|
||||||
|
</div>
|
||||||
|
<!-- MainContent End -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Content End -->
|
||||||
|
|
||||||
|
<!-- BrandBar Start -->
|
||||||
|
<brand-bar></brand-bar>
|
||||||
|
<!-- BrandBar End -->
|
||||||
|
|
||||||
|
<!-- ContactBar Start -->
|
||||||
|
<contact-bar></contact-bar>
|
||||||
|
<!-- ContactBar End -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Apps from './components/Apps.vue'
|
||||||
|
import BrandBar from './components/BrandBar.vue'
|
||||||
|
import ContactBar from './components/ContactBar.vue'
|
||||||
|
import SearchBar from './components/SearchBar.vue'
|
||||||
|
import SideBar from './components/SideBar.vue'
|
||||||
|
import Suggestion from './components/Suggestion.vue'
|
||||||
|
import TopBar from './components/TopBar.vue'
|
||||||
|
//import Shortcuts from './components/Shortcuts.vue'
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
BrandBar,
|
||||||
|
ContactBar,
|
||||||
|
SideBar,
|
||||||
|
SearchBar,
|
||||||
|
Suggestion,
|
||||||
|
Apps,
|
||||||
|
TopBar,
|
||||||
|
//Shortcuts
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// Check if not login then login and get token
|
||||||
|
if (!localStorage.getItem("user_token")) {
|
||||||
|
this.login()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
login() {
|
||||||
|
/**
|
||||||
|
* @description: Login
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
// this.$api.user.login({
|
||||||
|
// username: "admin",
|
||||||
|
// pwd: "admin"
|
||||||
|
// }).then((res) => {
|
||||||
|
// if (res.data.success == 200) {
|
||||||
|
// localStorage.setItem("user_token", res.data.data)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
BIN
UI/src/assets/background/AbstractShapes.jpg
Normal file
After Width: | Height: | Size: 5.3 MiB |
11
UI/src/assets/img/casa.svg
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:none;stroke:#FFFFFF;stroke-width:2;stroke-linejoin:round;stroke-miterlimit:2;}
|
||||||
|
</style>
|
||||||
|
<path class="st0" d="M12,22c5.5,0,10-4.5,10-10S17.5,2,12,2S2,6.5,2,12S6.5,22,12,22z"/>
|
||||||
|
<path class="st0" d="M12,22c3.9,0,7-3.1,7-7s-3.1-7-7-7s-7,3.1-7,7S8.1,22,12,22z"/>
|
||||||
|
<path class="st0" d="M12,22c2.2,0,4-1.8,4-4s-1.8-4-4-4s-4,1.8-4,4S9.8,22,12,22z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 729 B |
BIN
UI/src/assets/img/default.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
UI/src/assets/img/icon.png
Executable file
After Width: | Height: | Size: 21 KiB |
1
UI/src/assets/img/wuji.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="48" height="48" fill="white" fill-opacity="0.01"/><path d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z" stroke="#333" stroke-width="4" stroke-linejoin="round"/><path d="M24 44C31.732 44 38 37.732 38 30C38 22.268 31.732 16 24 16C16.268 16 10 22.268 10 30C10 37.732 16.268 44 24 44Z" stroke="#333" stroke-width="4" stroke-linejoin="round"/><path d="M24 44C28.4183 44 32 40.4183 32 36C32 31.5817 28.4183 28 24 28C19.5817 28 16 31.5817 16 36C16 40.4183 19.5817 44 24 44Z" fill="none" stroke="#333" stroke-width="4" stroke-linejoin="round"/></svg>
|
After Width: | Height: | Size: 758 B |
BIN
UI/src/assets/logo.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
152
UI/src/assets/scss/_variables.scss
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
// Included below are all the defined variables from Bulma
|
||||||
|
// Modify as needed, removing the !default attribute.
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
|
||||||
|
$black: hsl(0, 0%, 4%) !default;
|
||||||
|
$black-bis: hsl(0, 0%, 7%) !default;
|
||||||
|
$black-ter: hsl(0, 0%, 14%) !default;
|
||||||
|
|
||||||
|
$grey-darker: hsl(0, 0%, 21%) !default;
|
||||||
|
$grey-dark: hsl(0, 0%, 29%) !default;
|
||||||
|
$grey: hsl(0, 0%, 48%) !default;
|
||||||
|
$grey-light: hsl(0, 0%, 71%) !default;
|
||||||
|
$grey-lighter: hsl(0, 0%, 86%) !default;
|
||||||
|
|
||||||
|
$white-ter: hsl(0, 0%, 96%) !default;
|
||||||
|
$white-bis: hsl(0, 0%, 98%) !default;
|
||||||
|
$white: hsl(0, 0%, 100%) !default;
|
||||||
|
|
||||||
|
$orange: hsl(14, 100%, 53%) !default;
|
||||||
|
$yellow: hsl(48, 100%, 67%) !default;
|
||||||
|
$green: hsl(141, 71%, 48%) !default;
|
||||||
|
$turquoise: hsl(171, 100%, 41%) !default;
|
||||||
|
$cyan: hsl(204, 86%, 53%) !default;
|
||||||
|
$blue: hsl(217, 71%, 53%) !default;
|
||||||
|
$purple: hsl(271, 100%, 71%) !default;
|
||||||
|
$red: hsl(348, 100%, 61%) !default;
|
||||||
|
|
||||||
|
// Typography
|
||||||
|
|
||||||
|
$family-sans-serif: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif !default;
|
||||||
|
$family-monospace: monospace !default;
|
||||||
|
$render-mode: optimizeLegibility !default;
|
||||||
|
|
||||||
|
$size-1: 3rem !default;
|
||||||
|
$size-2: 2.5rem !default;
|
||||||
|
$size-3: 2rem !default;
|
||||||
|
$size-4: 1.5rem !default;
|
||||||
|
$size-5: 1.25rem !default;
|
||||||
|
$size-6: 1rem !default;
|
||||||
|
$size-7: 0.75rem !default;
|
||||||
|
|
||||||
|
$weight-light: 300 !default;
|
||||||
|
$weight-normal: 400 !default;
|
||||||
|
$weight-medium: 500 !default;
|
||||||
|
$weight-semibold: 600 !default;
|
||||||
|
$weight-bold: 700 !default;
|
||||||
|
|
||||||
|
// Responsiveness
|
||||||
|
|
||||||
|
// The container horizontal gap, which acts as the offset for breakpoints
|
||||||
|
$gap: 32px !default;
|
||||||
|
// 960, 1152, and 1344 have been chosen because they are divisible by both 12 and 16
|
||||||
|
$tablet: 769px !default;
|
||||||
|
// 960px container + 4rem
|
||||||
|
$desktop: 960px + (2 * $gap) !default;
|
||||||
|
// 1152px container + 4rem
|
||||||
|
$widescreen: 1152px + (2 * $gap) !default;
|
||||||
|
// 1344px container + 4rem;
|
||||||
|
$fullhd: 1344px + (2 * $gap) !default;
|
||||||
|
|
||||||
|
// Miscellaneous
|
||||||
|
|
||||||
|
$easing: ease-out !default;
|
||||||
|
$radius-small: 2px !default;
|
||||||
|
$radius: 3px !default;
|
||||||
|
$radius-large: 5px !default;
|
||||||
|
$radius-rounded: 290486px !default;
|
||||||
|
$speed: 86ms !default;
|
||||||
|
|
||||||
|
// Flags
|
||||||
|
|
||||||
|
$variable-columns: true !default;
|
||||||
|
|
||||||
|
|
||||||
|
// The default Bulma derived variables are declared below
|
||||||
|
|
||||||
|
$primary: $turquoise !default;
|
||||||
|
|
||||||
|
$info: $cyan !default;
|
||||||
|
$success: $green !default;
|
||||||
|
$warning: $yellow !default;
|
||||||
|
$danger: $red !default;
|
||||||
|
|
||||||
|
$light: $white-ter !default;
|
||||||
|
$dark: $grey-darker !default;
|
||||||
|
|
||||||
|
// Invert colors
|
||||||
|
|
||||||
|
$orange-invert: findColorInvert($orange) !default;
|
||||||
|
$yellow-invert: findColorInvert($yellow) !default;
|
||||||
|
$green-invert: findColorInvert($green) !default;
|
||||||
|
$turquoise-invert: findColorInvert($turquoise) !default;
|
||||||
|
$cyan-invert: findColorInvert($cyan) !default;
|
||||||
|
$blue-invert: findColorInvert($blue) !default;
|
||||||
|
$purple-invert: findColorInvert($purple) !default;
|
||||||
|
$red-invert: findColorInvert($red) !default;
|
||||||
|
|
||||||
|
$primary-invert: $turquoise-invert !default;
|
||||||
|
$info-invert: $cyan-invert !default;
|
||||||
|
$success-invert: $green-invert !default;
|
||||||
|
$warning-invert: $yellow-invert !default;
|
||||||
|
$danger-invert: $red-invert !default;
|
||||||
|
$light-invert: $dark !default;
|
||||||
|
$dark-invert: $light !default;
|
||||||
|
|
||||||
|
// General colors
|
||||||
|
|
||||||
|
$background: $white-ter !default;
|
||||||
|
|
||||||
|
$border: $grey-lighter !default;
|
||||||
|
$border-hover: $grey-light !default;
|
||||||
|
|
||||||
|
// Text colors
|
||||||
|
|
||||||
|
$text: $grey-dark !default;
|
||||||
|
$text-invert: findColorInvert($text) !default;
|
||||||
|
$text-light: $grey !default;
|
||||||
|
$text-strong: $grey-darker !default;
|
||||||
|
|
||||||
|
// Code colors
|
||||||
|
|
||||||
|
$code: $red !default;
|
||||||
|
$code-background: $background !default;
|
||||||
|
|
||||||
|
$pre: $text !default;
|
||||||
|
$pre-background: $background !default;
|
||||||
|
|
||||||
|
// Link colors
|
||||||
|
|
||||||
|
$link: $blue !default;
|
||||||
|
$link-invert: $blue-invert !default;
|
||||||
|
$link-visited: $purple !default;
|
||||||
|
|
||||||
|
$link-hover: $grey-darker !default;
|
||||||
|
$link-hover-border: $grey-light !default;
|
||||||
|
|
||||||
|
$link-focus: $grey-darker !default;
|
||||||
|
$link-focus-border: $blue !default;
|
||||||
|
|
||||||
|
$link-active: $grey-darker !default;
|
||||||
|
$link-active-border: $grey-dark !default;
|
||||||
|
|
||||||
|
// Typography
|
||||||
|
|
||||||
|
$family-primary: $family-sans-serif !default;
|
||||||
|
$family-code: $family-monospace !default;
|
||||||
|
|
||||||
|
$size-small: $size-7 !default;
|
||||||
|
$size-normal: $size-6 !default;
|
||||||
|
$size-medium: $size-5 !default;
|
||||||
|
$size-large: $size-4 !default;
|
429
UI/src/assets/scss/app.scss
Normal file
|
@ -0,0 +1,429 @@
|
||||||
|
@import "~bulma/sass/utilities/initial-variables";
|
||||||
|
@import "~bulma/sass/utilities/functions";
|
||||||
|
// 1. Set your own initial variables and derived
|
||||||
|
// variables in _variables.scss
|
||||||
|
@import "variables";
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;1,100;1,300;1,400;1,500;1,700&display=swap");
|
||||||
|
|
||||||
|
// 2. Setup your Custom Colors
|
||||||
|
$linkedin: #0077b5;
|
||||||
|
$linkedin-invert: findColorInvert($linkedin);
|
||||||
|
$twitter: #55acee;
|
||||||
|
$twitter-invert: findColorInvert($twitter);
|
||||||
|
$github: #333;
|
||||||
|
$github-invert: findColorInvert($github);
|
||||||
|
|
||||||
|
@import "~bulma/sass/utilities/derived-variables";
|
||||||
|
|
||||||
|
// 3. Add new color variables to the color map.
|
||||||
|
$addColors: (
|
||||||
|
"twitter": (
|
||||||
|
$twitter,
|
||||||
|
$twitter-invert,
|
||||||
|
),
|
||||||
|
"linkedin": (
|
||||||
|
$linkedin,
|
||||||
|
$linkedin-invert,
|
||||||
|
),
|
||||||
|
"github": (
|
||||||
|
$github,
|
||||||
|
$github-invert,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$colors: map-merge($colors, $addColors);
|
||||||
|
|
||||||
|
@import "~bulma";
|
||||||
|
@import "~buefy/src/scss/buefy";
|
||||||
|
|
||||||
|
$backDropColor: rgba(123, 123, 123, 0.16);
|
||||||
|
$backDropBlur: blur(1rem);
|
||||||
|
$backDropBorderRadius: 0.5rem;
|
||||||
|
|
||||||
|
// 4. Provide custom buefy overrides and site styles here
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-align: center;
|
||||||
|
color: #2c3e50;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
height: 3rem;
|
||||||
|
background: rgba(255, 255, 255, 0.22);
|
||||||
|
backdrop-filter: blur(180.282px);
|
||||||
|
.navbar-brand {
|
||||||
|
.dropdown-menu {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
min-width: 20rem;
|
||||||
|
.dropdown-content {
|
||||||
|
.dropdown-item {
|
||||||
|
padding: 1.25rem;
|
||||||
|
text-align: left;
|
||||||
|
.item {
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
.switch {
|
||||||
|
&.is-flex-direction-row-reverse {
|
||||||
|
.control-label {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: calc(0.75em - 1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.update-container {
|
||||||
|
.button.is-rounded {
|
||||||
|
border-radius: 9999px !important;
|
||||||
|
padding-left: calc(1em + 0.25em);
|
||||||
|
padding-right: calc(1em + 0.25em);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.button{
|
||||||
|
&.is-small{
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-bar {
|
||||||
|
position: fixed;
|
||||||
|
left: 2rem;
|
||||||
|
bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-bar {
|
||||||
|
position: fixed;
|
||||||
|
right: 2rem;
|
||||||
|
bottom: 2rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
background: rgba(0, 0, 0, 0.16);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
a {
|
||||||
|
color: $white;
|
||||||
|
margin: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
&:hover {
|
||||||
|
color: #0077b5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contents {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-bar {
|
||||||
|
width: 16rem;
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 17.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pt-7 {
|
||||||
|
padding-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pt-55 {
|
||||||
|
padding-top: 2rem;
|
||||||
|
}
|
||||||
|
.p-55 {
|
||||||
|
padding: 2rem !important;
|
||||||
|
}
|
||||||
|
.button.is-light {
|
||||||
|
background-color: #a6afb9;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
.input,
|
||||||
|
.textarea,
|
||||||
|
.taginput .taginput-container.is-focusable,
|
||||||
|
.select select,
|
||||||
|
.file-cta,
|
||||||
|
.file-name,
|
||||||
|
.pagination-previous,
|
||||||
|
.pagination-next,
|
||||||
|
.pagination-link,
|
||||||
|
.pagination-ellipsis {
|
||||||
|
&:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.image.is-72x72 {
|
||||||
|
height: 72px;
|
||||||
|
width: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// widgets
|
||||||
|
.widget {
|
||||||
|
background: $backDropColor;
|
||||||
|
backdrop-filter: $backDropBlur;
|
||||||
|
border-radius: $backDropBorderRadius;
|
||||||
|
padding: 0.875rem 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cards
|
||||||
|
.wuji-card {
|
||||||
|
background: $backDropColor;
|
||||||
|
backdrop-filter: $backDropBlur;
|
||||||
|
border-radius: $backDropBorderRadius;
|
||||||
|
padding: 1.5rem;
|
||||||
|
color: $white;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.info {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 1rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simg {
|
||||||
|
img {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.icon-img {
|
||||||
|
position: relative;
|
||||||
|
&.stop::after {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
width: 0.75rem;
|
||||||
|
height: 0.75rem;
|
||||||
|
background-color: #ff1616;
|
||||||
|
border-radius: 50%;
|
||||||
|
right: -0.375rem;
|
||||||
|
top: -0.375rem;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.b-image-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
&.stop::after {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
width: 0.75rem;
|
||||||
|
height: 0.75rem;
|
||||||
|
background-color: #ff1616;
|
||||||
|
border-radius: 50%;
|
||||||
|
right: -0.375rem;
|
||||||
|
top: -0.375rem;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.action-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.5rem;
|
||||||
|
top: 1rem;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.one-line {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.two-line {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
.action-btn {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: white;
|
||||||
|
p {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flex1 {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.title-bar {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
.title {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ii {
|
||||||
|
.dropdown-menu {
|
||||||
|
background: rgba(255, 255, 255, 0.88);
|
||||||
|
backdrop-filter: $backDropBlur;
|
||||||
|
border-radius: $backDropBorderRadius;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-top: 0;
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
.button {
|
||||||
|
border-radius: 0;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
&.is-text {
|
||||||
|
text-decoration: none;
|
||||||
|
justify-content: flex-start;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: none !important;
|
||||||
|
&.running {
|
||||||
|
color: #779e2a !important;
|
||||||
|
}
|
||||||
|
&.exited {
|
||||||
|
color: #ff1616 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bbor {
|
||||||
|
overflow: hidden;
|
||||||
|
border-top: #2c3e50 1px solid;
|
||||||
|
.is-text {
|
||||||
|
text-decoration: none;
|
||||||
|
justify-content: center !important ;
|
||||||
|
}
|
||||||
|
.column:first-child {
|
||||||
|
border-right: #2c3e50 1px solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Panel
|
||||||
|
.modal-background {
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
.modal-card {
|
||||||
|
background: rgba(255, 255, 255, 0.88);
|
||||||
|
backdrop-filter: $backDropBlur;
|
||||||
|
border-radius: $backDropBorderRadius;
|
||||||
|
.modal-card-head,
|
||||||
|
.modal-card-body,
|
||||||
|
.modal-card-foot {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.modal-card-head {
|
||||||
|
padding: 3rem;
|
||||||
|
}
|
||||||
|
.modal-card-body {
|
||||||
|
padding: 0 3rem;
|
||||||
|
.button.is-static,
|
||||||
|
.input,
|
||||||
|
.textarea,
|
||||||
|
.taginput .taginput-container.is-focusable,
|
||||||
|
.select select,
|
||||||
|
.file-cta,
|
||||||
|
.file-name,
|
||||||
|
.pagination-previous,
|
||||||
|
.pagination-next,
|
||||||
|
.pagination-link,
|
||||||
|
.pagination-ellipsis {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
height: 2.714em;
|
||||||
|
border: 1px solid #cfcfcf !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
&:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.media {
|
||||||
|
padding: 0rem;
|
||||||
|
}
|
||||||
|
.field:last-child {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.field-body {
|
||||||
|
.field:last-child {
|
||||||
|
margin-bottom: 0rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.port-item:not(:last-child) {
|
||||||
|
.field {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.modal-card-foot {
|
||||||
|
padding: 1rem 3rem 2rem 3rem;
|
||||||
|
.button {
|
||||||
|
border-radius: 9999px;
|
||||||
|
padding-left: calc(1em + 0.25em);
|
||||||
|
padding-right: calc(1em + 0.25em);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-area .textarea {
|
||||||
|
max-height: 40em;
|
||||||
|
min-height: 16em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card {
|
||||||
|
.loading-background {
|
||||||
|
background: none !important;
|
||||||
|
border-radius: $backDropBorderRadius;
|
||||||
|
}
|
||||||
|
}
|
53
UI/src/assets/widgets/Clock.vue
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<div class="widget has-text-white clock">
|
||||||
|
<div class="time">{{timeText}}</div>
|
||||||
|
<div class="data">{{dateText}}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import moment from 'moment'
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
timer: 0,
|
||||||
|
timeText:"",
|
||||||
|
dateText:""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.timer) {
|
||||||
|
clearInterval(this.timer)
|
||||||
|
}
|
||||||
|
this.updateClock()
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
this.updateClock()
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateClock() {
|
||||||
|
|
||||||
|
this.timeText = moment().format('LT');
|
||||||
|
this.dateText = moment().format('dddd, MMMM Do')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.clock {
|
||||||
|
font-family: Roboto;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: left;
|
||||||
|
.time {
|
||||||
|
font-size: 2.75rem;
|
||||||
|
line-height: 4.25rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.data {
|
||||||
|
line-height: 1.5rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
16
UI/src/assets/widgets/Weather.vue
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
<div class="widget has-text-white clock">
|
||||||
|
<div class="time">09:40</div>
|
||||||
|
<div class="data">Wednesday,September 15</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
128
UI/src/components/Apps.vue
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
<!--
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-18 23:01:19
|
||||||
|
* @Description: App module
|
||||||
|
* @FilePath: \CasaOS-UI\src\components\Apps.vue
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="has-text-left mt-6">
|
||||||
|
<!-- Title Bar Start -->
|
||||||
|
<div class="title-bar is-flex is-align-items-center">
|
||||||
|
<h1 class="title is-4 has-text-white is-flex-shrink-1">Apps</h1>
|
||||||
|
<div class="buttons ">
|
||||||
|
<b-button icon-left="plus" type="is-dark" size="is-small" rounded @click="showInstall">New App</b-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Title Bar End -->
|
||||||
|
|
||||||
|
<!-- App List Start -->
|
||||||
|
<div class="columns is-variable is-2 is-multiline ">
|
||||||
|
<div class="column is-narrow is-3" v-for="(item,index) in appList" :key="'app-'+index">
|
||||||
|
<app-card :item="item" @updateState="getList" @configApp="showConfigPanel"></app-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Title Bar End -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import AppCard from './Apps/AppCard.vue'
|
||||||
|
import Panel from './Panel.vue'
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
appList: [],
|
||||||
|
appConfig: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
AppCard,
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Fetch the list of installed apps
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
getList() {
|
||||||
|
this.$api.app.myAppList().then(res => {
|
||||||
|
this.appList = res.data.data;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Show Install Panel Programmatic
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
showInstall() {
|
||||||
|
this.$api.app.appConfig().then(res => {
|
||||||
|
if (res.data.success == 200) {
|
||||||
|
this.$buefy.modal.open({
|
||||||
|
parent: this,
|
||||||
|
component: Panel,
|
||||||
|
hasModalCard: true,
|
||||||
|
customClass: '',
|
||||||
|
trapFocus: true,
|
||||||
|
canCancel: ['escape'],
|
||||||
|
scroll: "keep",
|
||||||
|
animation: "zoom-out",
|
||||||
|
events: {
|
||||||
|
'updateState': () => {
|
||||||
|
this.getList()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
id: "0",
|
||||||
|
state: "install",
|
||||||
|
configData: res.data.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Show Settings Panel Programmatic
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
showConfigPanel(id) {
|
||||||
|
this.$api.app.getContainerSettingdata(id).then(ret => {
|
||||||
|
this.$api.app.appConfig().then(res => {
|
||||||
|
if (res.data.success == 200) {
|
||||||
|
this.$buefy.modal.open({
|
||||||
|
parent: this,
|
||||||
|
component: Panel,
|
||||||
|
hasModalCard: true,
|
||||||
|
customClass: '',
|
||||||
|
trapFocus: true,
|
||||||
|
canCancel: ['escape'],
|
||||||
|
scroll: "keep",
|
||||||
|
animation: "zoom-out",
|
||||||
|
events: {
|
||||||
|
'updateState': () => {
|
||||||
|
this.getList()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
id: id,
|
||||||
|
state: "update",
|
||||||
|
configData: res.data.data,
|
||||||
|
initDatas: ret.data.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
202
UI/src/components/Apps/AppCard.vue
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
<!--
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-18 23:12:57
|
||||||
|
* @Description: App Card item
|
||||||
|
* @FilePath: \CasaOS-UI\src\components\Apps\AppCard.vue
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="wuji-card is-flex is-align-items-center is-justify-content-center p-55 app-card" @mouseover="hover = true" @mouseleave="hover = false">
|
||||||
|
<!-- Action Button Start -->
|
||||||
|
<div class="action-btn">
|
||||||
|
<b-dropdown aria-role="list" position="is-bottom-left" class="ii" ref="dro" @active-change="setDropState">
|
||||||
|
<template #trigger>
|
||||||
|
<p role="button">
|
||||||
|
<b-icon pack="fas" icon="ellipsis-v" size="is-small">
|
||||||
|
</b-icon>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<b-dropdown-item aria-role="menu-item" :focusable="false" custom paddingless>
|
||||||
|
<b-button type="is-text" tag="a" :target="(item.state == 'running') ?'_blank':'_self'" :href="(item.state == 'running') ? siteUrl(item.port,item.index) :'javascript:void(0)'" expanded>Open</b-button>
|
||||||
|
<b-button type="is-text" @click="configApp" expanded>Setting</b-button>
|
||||||
|
<b-button type="is-text" expanded @click="uninstallConfirm" :loading="isUninstalling">Unistall</b-button>
|
||||||
|
<div class="columns is-gapless bbor">
|
||||||
|
<div class="column is-flex is-justify-content-center is-align-items-center">
|
||||||
|
<b-button icon-pack="fas" icon-left="sync" type="is-text" expanded :loading="isRestarting" @click="restartApp"></b-button>
|
||||||
|
</div>
|
||||||
|
<div class="column is-flex is-justify-content-center is-align-items-center">
|
||||||
|
<b-button icon-pack="fas" icon-left="power-off" type="is-text" expanded @click="toggle(item)" :loading="isStarting" :class="item.state"></b-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-dropdown-item>
|
||||||
|
</b-dropdown>
|
||||||
|
</div>
|
||||||
|
<!-- Action Button End -->
|
||||||
|
|
||||||
|
<!-- Card Content Start -->
|
||||||
|
<div class="has-text-centered is-flex is-justify-content-center is-flex-direction-column pt-3 pb-3">
|
||||||
|
<a :target="(item.state == 'running') ?'_blank':'_self'" class="is-flex is-justify-content-center" :href="(item.state == 'running') ? siteUrl(item.port,item.index) :'javascript:void(0)'">
|
||||||
|
<b-image :src="item.icon" :src-fallback="require('@/assets/img/default.png')" webp-fallback=".jpg" class="is-72x72" :class="item.state | dotClass"></b-image>
|
||||||
|
</a>
|
||||||
|
<p class="mt-4 one-line">
|
||||||
|
<a class="one-line" :target="(item.state == 'running') ?'_blank':'_self'" :href="(item.state == 'running') ? siteUrl(item.port,item.index) :'javascript:void(0)'">
|
||||||
|
{{item.name}}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- Card Content End -->
|
||||||
|
|
||||||
|
<!-- Loading Bar Start -->
|
||||||
|
<b-loading :is-full-page="false" v-model="isUninstalling" :can-cancel="false"></b-loading>
|
||||||
|
<!-- Loading Bar End -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "app-card",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hover: false,
|
||||||
|
dropState: false,
|
||||||
|
isUninstalling: false,
|
||||||
|
isRestarting: false,
|
||||||
|
isStarting: false,
|
||||||
|
isStoping: false,
|
||||||
|
isSaving: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* @description: Create application access link
|
||||||
|
* @param {String} port App access port
|
||||||
|
* @param {String} index App access index page
|
||||||
|
* @return {String}
|
||||||
|
*/
|
||||||
|
siteUrl(port, index) {
|
||||||
|
return (process.env.NODE_ENV === "'dev'") ? `http://${this.$store.state.devIp}:${port}${index}` : `http://${document.domain}:${port}${index}`
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Set drop-down menu status
|
||||||
|
* @param {Boolean} e
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
setDropState(e) {
|
||||||
|
this.dropState = e
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Restart Application
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
restartApp() {
|
||||||
|
this.isRestarting = true
|
||||||
|
this.$api.app.startContainer(this.item.custom_id, { state: "restart" }).then((res) => {
|
||||||
|
console.log(res.data);
|
||||||
|
if (res.data.success == 200) {
|
||||||
|
this.updateState()
|
||||||
|
}
|
||||||
|
this.isRestarting = false;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Confirm before uninstall
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
uninstallConfirm() {
|
||||||
|
let _this = this
|
||||||
|
this.$buefy.dialog.confirm({
|
||||||
|
title: 'Attention',
|
||||||
|
message: 'Data cannot be recovered after deletion! <br>Continue on to uninstall this application?',
|
||||||
|
type: 'is-dark',
|
||||||
|
confirmText: 'Uninstall',
|
||||||
|
onConfirm: () => {
|
||||||
|
_this.isUninstalling = true
|
||||||
|
_this.uninstallApp()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Uninstall app
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
uninstallApp() {
|
||||||
|
this.isUninstalling = true
|
||||||
|
this.$api.app.uninstall(this.item.custom_id).then((res) => {
|
||||||
|
if (res.data.success == 200) {
|
||||||
|
console.log(res.data.data);
|
||||||
|
this.updateState()
|
||||||
|
}
|
||||||
|
this.isUninstalling = false;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Emit the event that the app has been updated
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
updateState() {
|
||||||
|
this.$emit("updateState")
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Emit the event that the app has been updated with custom_id
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
configApp() {
|
||||||
|
this.$emit("configApp", this.item.custom_id)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Start or Stop a App
|
||||||
|
* @param {Object} item the app info object
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
toggle(item) {
|
||||||
|
this.isStarting = true;
|
||||||
|
let data = {
|
||||||
|
state: item.state == "running" ? "stop" : "start"
|
||||||
|
}
|
||||||
|
this.$api.app.startContainer(item.custom_id, data).then((res) => {
|
||||||
|
console.log(res.data);
|
||||||
|
item.state = res.data.data
|
||||||
|
this.isStarting = false
|
||||||
|
this.updateState()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
hover(val) {
|
||||||
|
if (!val && this.dropState)
|
||||||
|
this.$refs.dro.toggle();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
/**
|
||||||
|
* @description: Format Dot Class
|
||||||
|
* @param {String} state
|
||||||
|
* @return {String}
|
||||||
|
*/
|
||||||
|
dotClass(state) {
|
||||||
|
return state == 'running' ? 'start' : 'stop'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
27
UI/src/components/BrandBar.vue
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<!--
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-18 23:13:58
|
||||||
|
* @Description: The left bottom brand bar
|
||||||
|
* @FilePath: \CasaOS-UI\src\components\BrandBar.vue
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="brand-bar is-flex is-align-items-center has-text-white">
|
||||||
|
<figure class="image is-32x32">
|
||||||
|
<img :src="require('@/assets/img/casa.svg')">
|
||||||
|
</figure>
|
||||||
|
<span class="is-size-4 mr-3 ml-3">CasaOS</span>
|
||||||
|
<span>Made by IceWhale with ❤ and you !️</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "brand-bar"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
28
UI/src/components/ContactBar.vue
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<!--
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-18 23:14:33
|
||||||
|
* @Description: The right bottom contact bar
|
||||||
|
* @FilePath: \CasaOS-UI\src\components\ContactBar.vue
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="contact-bar is-flex is-align-items-center has-text-white pl-3 pr-3">
|
||||||
|
<a href="#">
|
||||||
|
<b-icon pack="fas" icon="map-signs" size=""></b-icon>
|
||||||
|
</a>
|
||||||
|
<a href="https://discord.gg/Gx4BCEtHjx" target="_blank">
|
||||||
|
<b-icon pack="fab" icon="discord" size=""></b-icon>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ZimaBoard/CasaOS" target="_blank">
|
||||||
|
<b-icon pack="fab" icon="github" size=""></b-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "contact-bar"
|
||||||
|
}
|
||||||
|
</script>
|
453
UI/src/components/Panel.vue
Normal file
|
@ -0,0 +1,453 @@
|
||||||
|
<!--
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-22 16:27:00
|
||||||
|
* @Description: Install Panel of Docker
|
||||||
|
* @FilePath: /CasaOS-UI/src/components/Panel.vue
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="modal-card">
|
||||||
|
<!-- Modal-Card Header Start -->
|
||||||
|
<header class="modal-card-head">
|
||||||
|
<div class="flex1">
|
||||||
|
<h3 class="title is-4 has-text-weight-normal">Create a new App manually</h3>
|
||||||
|
</div>
|
||||||
|
<b-button icon-left="file-import" type="is-dark" size="is-small" rounded @click="showImportPanel" v-if="currentSlide == 1 && state == 'install'">Import</b-button>
|
||||||
|
</header>
|
||||||
|
<!-- Modal-Card Header End -->
|
||||||
|
<!-- Modal-Card Body Start -->
|
||||||
|
<section class="modal-card-body">
|
||||||
|
|
||||||
|
<section v-show="currentSlide == 1">
|
||||||
|
<ValidationObserver ref="ob1">
|
||||||
|
<ValidationProvider rules="required" name="Image" v-slot="{ errors, valid }">
|
||||||
|
<b-field label="Docker Image *" :type="{ 'is-danger': errors[0], 'is-success': valid }" :message="errors">
|
||||||
|
<b-input v-model="initData.image" placeholder="e.g.,hello-world:latest" :readonly="state == 'update'"></b-input>
|
||||||
|
<!-- <b-autocomplete :data="data" placeholder="e.g. hello-world:latest" field="image" :loading="isFetching" @typing="getAsyncData" @select="option => selected = option" v-model="initData.image" :readonly="state == 'update'"></b-autocomplete> -->
|
||||||
|
</b-field>
|
||||||
|
</ValidationProvider>
|
||||||
|
<ValidationProvider rules="required" name="Name" v-slot="{ errors, valid }">
|
||||||
|
<b-field label="App name *" :type="{ 'is-danger': errors[0], 'is-success': valid }" :message="errors">
|
||||||
|
<b-input value="" v-model="initData.label" placeholder="Your custom App Name"></b-input>
|
||||||
|
</b-field>
|
||||||
|
</ValidationProvider>
|
||||||
|
|
||||||
|
<b-field label="Icon URL">
|
||||||
|
<b-input value="" v-model="initData.icon" placeholder="Your custom icon URL"></b-input>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Web UI">
|
||||||
|
<p class="control">
|
||||||
|
<span class="button is-static">{{baseUrl}}</span>
|
||||||
|
</p>
|
||||||
|
<b-input v-model="webui" placeholder="8080/web/index.html" expanded></b-input>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Network">
|
||||||
|
<b-select v-model="initData.network_model" placeholder="Select" expanded>
|
||||||
|
<optgroup v-for="net in networks" :key="net.driver" :label="net.driver">
|
||||||
|
<option v-for="(option,index) in net.networks" :value="option.id" :key="option.name+index">
|
||||||
|
{{ option.name}}
|
||||||
|
</option>
|
||||||
|
</optgroup>
|
||||||
|
</b-select>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<ports v-model="initData.ports" :showHostPost="showHostPort" v-if="showPorts"></ports>
|
||||||
|
<input-group v-model="initData.volumes" label="Data Volumes" message="No App Data Volumes now, Click “+” to add one."></input-group>
|
||||||
|
<input-group v-model="initData.envs" label="Environment Variables" message="No environment variables now, Click “+” to add one." name1="Key" name2="Value"></input-group>
|
||||||
|
<input-group v-model="initData.devices" label="Devices" message="No devices now, Click “+” to add one."></input-group>
|
||||||
|
<b-field label="Memory Limit">
|
||||||
|
<vue-slider :min="256" :max="totalMemory" v-model="initData.memory"></vue-slider>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="CPU Shares">
|
||||||
|
<b-select v-model="initData.cpu_shares" placeholder="Select" expanded>
|
||||||
|
<option value="10">Low</option>
|
||||||
|
<option value="50">Medium</option>
|
||||||
|
<option value="90">High</option>
|
||||||
|
</b-select>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
<b-field label="Restart Policy">
|
||||||
|
<b-select v-model="initData.restart" placeholder="Select" expanded>
|
||||||
|
<option value="on-failure">on-failure</option>
|
||||||
|
<option value="always">always</option>
|
||||||
|
<option value="unless-stopped">unless-stopped</option>
|
||||||
|
</b-select>
|
||||||
|
</b-field>
|
||||||
|
<b-field label="App Description">
|
||||||
|
<b-input v-model="initData.description"></b-input>
|
||||||
|
</b-field>
|
||||||
|
<b-loading :is-full-page="false" v-model="isLoading" :can-cancel="false"></b-loading>
|
||||||
|
</ValidationObserver>
|
||||||
|
</section>
|
||||||
|
<section v-show="currentSlide == 2">
|
||||||
|
<div class="installing-warpper">
|
||||||
|
<lottie-animation path="./ui/img/ani/rocket-launching.json" :autoPlay="true" :width="200" :height="200"></lottie-animation>
|
||||||
|
<h3 class="title is-6 has-text-centered" :class="{'has-text-danger':errorType == 3,'has-text-black':errorType != 3}" v-html="installText"></h3>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
<!-- Modal-Card Body End -->
|
||||||
|
|
||||||
|
<!-- Modal-Card Footer Start-->
|
||||||
|
<footer class="modal-card-foot is-flex is-align-items-center">
|
||||||
|
<div class="flex1"></div>
|
||||||
|
<div>
|
||||||
|
<b-button v-if="currentSlide == 1" :label="cancelButtonText" @click="$emit('close')" rounded />
|
||||||
|
<b-button v-if="currentSlide == 2 && errorType == 3 " label="Back" @click="prevStep" rounded />
|
||||||
|
<b-button v-if="currentSlide == 1 && state == 'install'" label="Install" type="is-dark" @click="installApp()" rounded />
|
||||||
|
<b-button v-if="currentSlide == 1 && state == 'update'" label="Update" type="is-dark" @click="updateApp()" rounded />
|
||||||
|
<b-button v-if="currentSlide == 2" :label="cancelButtonText" type="is-dark" @click="$emit('close')" rounded />
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<!-- Modal-Card Footer End -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios'
|
||||||
|
import InputGroup from './forms/InputGroup.vue';
|
||||||
|
import Ports from './forms/Ports.vue'
|
||||||
|
import ImportPanel from './forms/ImportPanel.vue'
|
||||||
|
import LottieAnimation from "lottie-vuejs/src/LottieAnimation.vue";
|
||||||
|
import VueSlider from 'vue-slider-component'
|
||||||
|
import 'vue-slider-component/theme/default.css'
|
||||||
|
import { ValidationObserver, ValidationProvider } from "vee-validate";
|
||||||
|
import "@/plugins/vee-validate";
|
||||||
|
import debounce from 'lodash/debounce'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Ports,
|
||||||
|
InputGroup,
|
||||||
|
ValidationObserver,
|
||||||
|
ValidationProvider,
|
||||||
|
LottieAnimation,
|
||||||
|
VueSlider
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
timer: 0,
|
||||||
|
data: [],
|
||||||
|
isLoading: false,
|
||||||
|
isFetching: false,
|
||||||
|
errorType: 1,
|
||||||
|
currentSlide: 1,
|
||||||
|
cancelButtonText: "Cancel",
|
||||||
|
webui: "",
|
||||||
|
baseUrl: "",
|
||||||
|
totalMemory: 0,
|
||||||
|
networks: [],
|
||||||
|
tempNetworks: [],
|
||||||
|
networkModes: [],
|
||||||
|
installPercent: 0,
|
||||||
|
installText: "",
|
||||||
|
|
||||||
|
initData: {
|
||||||
|
port_map: "",
|
||||||
|
cpu_shares: 10,
|
||||||
|
memory: 300,
|
||||||
|
restart: "always",
|
||||||
|
label: "",
|
||||||
|
position: true,
|
||||||
|
index: "",
|
||||||
|
icon: "",
|
||||||
|
network_model: "",
|
||||||
|
image: "",
|
||||||
|
description: "",
|
||||||
|
origin: "custom",
|
||||||
|
ports: [],
|
||||||
|
volumes: [],
|
||||||
|
envs: [],
|
||||||
|
devices: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
id: String,
|
||||||
|
state: String,
|
||||||
|
configData: Object,
|
||||||
|
initDatas: {
|
||||||
|
type: Object
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
//If it is edit, Init data
|
||||||
|
if (this.initDatas != undefined) {
|
||||||
|
this.initData = this.initDatas
|
||||||
|
this.webui = this.initDatas.port_map + this.initDatas.index
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get Max memory info form device
|
||||||
|
this.totalMemory = Math.floor(this.configData.memory.total / 1048576);
|
||||||
|
this.initData.memory = this.totalMemory
|
||||||
|
|
||||||
|
//Handling network types
|
||||||
|
this.tempNetworks = this.configData.networks;
|
||||||
|
this.networkModes = this.unique(this.tempNetworks.map(item => {
|
||||||
|
return item.driver
|
||||||
|
}))
|
||||||
|
this.networks = this.networkModes.map(item => {
|
||||||
|
let tempitem = {}
|
||||||
|
tempitem.driver = item
|
||||||
|
tempitem.networks = this.tempNetworks.filter(net => {
|
||||||
|
return net.driver == item
|
||||||
|
})
|
||||||
|
return tempitem
|
||||||
|
})
|
||||||
|
|
||||||
|
let gg = this.tempNetworks.filter(item => {
|
||||||
|
if (item.driver == "bridge") {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.initData.network_model = gg[0].id
|
||||||
|
|
||||||
|
// Set Front-end base url
|
||||||
|
this.baseUrl = `${window.location.protocol}//${document.domain}:`;
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
showPorts() {
|
||||||
|
if (this.initData.network_model.indexOf("macvlan") > -1) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showHostPort() {
|
||||||
|
if (this.initData.network_model.indexOf("host") > -1) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Process the datas before submit
|
||||||
|
* @param {*}
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
processData() {
|
||||||
|
// GET port map and index
|
||||||
|
if (this.webui != "") {
|
||||||
|
let slashArr = this.webui.split("/")
|
||||||
|
this.initData.port_map = slashArr[0]
|
||||||
|
this.initData.index = "/" + slashArr.slice(1).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
let model = this.initData.network_model.split("-");
|
||||||
|
this.initData.network_model = model[0]
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Array deduplication
|
||||||
|
* @param {Array} arr
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
|
unique(arr) {
|
||||||
|
for (var i = 0; i < arr.length; i++) {
|
||||||
|
for (var j = i + 1; j < arr.length; j++) {
|
||||||
|
if (arr[i] == arr[j]) {
|
||||||
|
arr.splice(j, 1);
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Back to prev Step
|
||||||
|
* @param {*}
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
prevStep() {
|
||||||
|
this.currentSlide--;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Validate form async
|
||||||
|
* @param {Object} ref ref of component
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
async checkStep(ref) {
|
||||||
|
let isValid = await ref.validate()
|
||||||
|
return isValid
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Submit datas after valid
|
||||||
|
* @param {*}
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
installApp() {
|
||||||
|
this.checkStep(this.$refs.ob1).then(val => {
|
||||||
|
if (val) {
|
||||||
|
this.processData();
|
||||||
|
this.isLoading = true;
|
||||||
|
this.$api.app.install(this.id, this.initData).then((res) => {
|
||||||
|
this.isLoading = false;
|
||||||
|
if (res.data.success == 200) {
|
||||||
|
this.currentSlide = 2;
|
||||||
|
this.cancelButtonText = "Continue in background"
|
||||||
|
this.checkInstallState(res.data.data)
|
||||||
|
} else {
|
||||||
|
//this.currentSlide = 1;
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: res.data.message,
|
||||||
|
type: 'is-warning'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Check the installation process every 250 milliseconds
|
||||||
|
* @param {String} appId
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
checkInstallState(appId) {
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
this.updateInstallState(appId)
|
||||||
|
}, 250)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Update the installation status to the UI
|
||||||
|
* @param {String} appId
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
updateInstallState(appId) {
|
||||||
|
this.$api.app.state(appId).then((res) => {
|
||||||
|
let resData = res.data.data;
|
||||||
|
this.installPercent = resData.speed;
|
||||||
|
this.errorType = resData.type;
|
||||||
|
if (this.errorType == 4) {
|
||||||
|
try {
|
||||||
|
let info = JSON.parse(resData.message)
|
||||||
|
let id = (info.id != undefined) ? info.id : "";
|
||||||
|
let progress = ""
|
||||||
|
if (info.progressDetail != undefined) {
|
||||||
|
let progressDetail = info.progressDetail
|
||||||
|
if (!isNaN(progressDetail.current / progressDetail.total)) {
|
||||||
|
progress = "<br>Progress:" + String(Math.floor((progressDetail.current / progressDetail.total) * 100)) + "%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let status = info.status
|
||||||
|
this.installText = status + ":" + id + " " + progress
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.installText = resData.message
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resData.speed == 100 || this.errorType == 3) {
|
||||||
|
clearInterval(this.timer)
|
||||||
|
}
|
||||||
|
let _this = this
|
||||||
|
if (resData.speed == 100) {
|
||||||
|
setTimeout(() => {
|
||||||
|
_this.$emit('updateState')
|
||||||
|
_this.$emit('close')
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Save edit update
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
updateApp() {
|
||||||
|
this.processData();
|
||||||
|
this.isLoading = true;
|
||||||
|
this.$api.app.updateContainerSetting(this.id, this.initData).then((res) => {
|
||||||
|
if (res.data.success == 200) {
|
||||||
|
this.isLoading = false;
|
||||||
|
this.$emit('updateState')
|
||||||
|
} else {
|
||||||
|
this.$buefy.toast.open({
|
||||||
|
message: res.data.message,
|
||||||
|
type: 'is-warning'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.$emit('close')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Show import panel
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
showImportPanel() {
|
||||||
|
this.$buefy.modal.open({
|
||||||
|
parent: this,
|
||||||
|
component: ImportPanel,
|
||||||
|
hasModalCard: true,
|
||||||
|
customClass: '',
|
||||||
|
trapFocus: true,
|
||||||
|
canCancel: ['escape'],
|
||||||
|
scroll: "keep",
|
||||||
|
animation: "zoom-out",
|
||||||
|
events: {
|
||||||
|
'update': (e) => {
|
||||||
|
this.initData = e
|
||||||
|
this.$buefy.dialog.alert({
|
||||||
|
title: 'Attention',
|
||||||
|
message: 'AutoFill only helps you to complete most of the configuration. Some of the configuration information still needs to be confirmed by you.',
|
||||||
|
type: 'is-dark'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
initData: this.initData,
|
||||||
|
netWorks: this.networks
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Get remote synchronization information
|
||||||
|
* @param {*} function
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
getAsyncData: debounce(function (name) {
|
||||||
|
if (!name.length) {
|
||||||
|
this.data = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.isFetching = true
|
||||||
|
axios.get(`https://hub.docker.com/api/content/v1/products/search?source=community&q=${name}&page=1&page_size=4`)
|
||||||
|
.then(({ data }) => {
|
||||||
|
this.data = []
|
||||||
|
data.summaries.forEach((item) => this.data.push(item.name))
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.data = []
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.isFetching = false
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
clearInterval(this.timer)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
105
UI/src/components/SearchBar.vue
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
<!--
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-19 09:23:01
|
||||||
|
* @Description: Top Search bar
|
||||||
|
* @FilePath: \CasaOS-UI\src\components\SearchBar.vue
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<b-field position="is-centered " class="search-bar has-text-white">
|
||||||
|
<b-input placeholder="Google Search..." v-model="keyText" icon="magnify" icon-right="magnify" icon-right-clickable @icon-right-click="gotoSearch" @keyup.enter.native="gotoSearch" size="is-large" :class="['ovh',isFocus?'fo':'']" expanded @focus="onFocus" @blur="onBlur">
|
||||||
|
</b-input>
|
||||||
|
</b-field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "search-bar",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isFocus: false,
|
||||||
|
keyText: ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* @description: Handle Focus event
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
onFocus() {
|
||||||
|
this.isFocus = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Handle Blur event
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
onBlur() {
|
||||||
|
if (this.keyText == "")
|
||||||
|
this.isFocus = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Pop up a new window and jump to google search
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
gotoSearch() {
|
||||||
|
window.open("https://www.google.com/search?q=" + this.keyText, '_blank')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.search-bar {
|
||||||
|
input {
|
||||||
|
transition: all 0.2s;
|
||||||
|
appearance: none;
|
||||||
|
background: rgba(123, 123, 123, 0.16);
|
||||||
|
backdrop-filter: blur(0.875rem);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: white;
|
||||||
|
&:focus {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
&::placeholder {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ovh {
|
||||||
|
overflow: hidden;
|
||||||
|
.icon.is-left {
|
||||||
|
transition: all 0.2s;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.icon.is-right {
|
||||||
|
transition: all 0.2s;
|
||||||
|
right: -3rem !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
padding-left: 2.5em !important;
|
||||||
|
padding-right: 1em !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fo {
|
||||||
|
.icon.is-left {
|
||||||
|
left: -3rem !important;
|
||||||
|
}
|
||||||
|
.icon.is-right {
|
||||||
|
transition: all 0.2s;
|
||||||
|
right: 0 !important;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
padding-right: 2.5em !important;
|
||||||
|
padding-left: 1em !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
56
UI/src/components/Shortcuts.vue
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<!--
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-19 09:23:49
|
||||||
|
* @Description:
|
||||||
|
* @FilePath: \CasaOS-UI\src\components\Shortcuts.vue
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="has-text-left mt-6">
|
||||||
|
<div class="title-bar is-flex is-align-items-center">
|
||||||
|
<h1 class="title is-4 has-text-white is-flex-shrink-1">Shortcuts</h1>
|
||||||
|
<div class="buttons ">
|
||||||
|
<b-button icon-left="plus" type="is-dark" size="is-small" rounded>Add shortcut</b-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="columns is-variable is-2 is-multiline ">
|
||||||
|
<div class="column is-narrow is-3" v-for="n in 10" :key="n">
|
||||||
|
<div class="wuji-card is-flex is-align-items-center ">
|
||||||
|
|
||||||
|
<figure class="image is-32x32 simg">
|
||||||
|
<img :src="require('@/assets/img/icon.png')">
|
||||||
|
</figure>
|
||||||
|
<p class="ml-4 flex1 one-line">Test</p>
|
||||||
|
<div class="action-btn1">
|
||||||
|
<b-dropdown aria-role="list" position="is-bottom-left" append-to-body>
|
||||||
|
<template #trigger>
|
||||||
|
<p role="button">
|
||||||
|
<b-icon pack="fas" icon="ellipsis-v" size="is-small">
|
||||||
|
</b-icon>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<b-dropdown-item aria-role="listitem">Action</b-dropdown-item>
|
||||||
|
<b-dropdown-item aria-role="listitem">Another action</b-dropdown-item>
|
||||||
|
<b-dropdown-item aria-role="listitem">Something else</b-dropdown-item>
|
||||||
|
</b-dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'shortcuts'
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
27
UI/src/components/SideBar.vue
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<!--
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-18 23:20:56
|
||||||
|
* @Description:
|
||||||
|
* @FilePath: \CasaOS-UI\src\components\SideBar.vue
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="side-bar mr-5">
|
||||||
|
<clock></clock>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Clock from '../assets/widgets/Clock.vue'
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'side-bar',
|
||||||
|
components: { Clock },
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
55
UI/src/components/Suggestion.vue
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<!--
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-18 23:20:19
|
||||||
|
* @Description:
|
||||||
|
* @FilePath: \CasaOS-UI\src\components\Suggestion.vue
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="has-text-left ">
|
||||||
|
<h1 class="title is-4 mt-6 has-text-white">Suggestions</h1>
|
||||||
|
<div class="columns is-variable is-2 is-multiline">
|
||||||
|
<div class="column is-one-third" v-for="(item,index) in list" :key="'ss'+index">
|
||||||
|
<a :href="item.url" target="_blank">
|
||||||
|
<div class="wuji-card is-flex is-align-items-center">
|
||||||
|
<div class="info ">
|
||||||
|
<div class="two-line ">
|
||||||
|
{{item.title}}
|
||||||
|
</div>
|
||||||
|
<div class="des two-line">
|
||||||
|
{{item.content}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<figure class="image is-48x48 simg is-flex">
|
||||||
|
<img :src="item.image_url">
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name:'suggestion',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
list: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$api.task.list().then(res => {
|
||||||
|
if (res.data.success == 200) {
|
||||||
|
this.list = res.data.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
161
UI/src/components/TopBar.vue
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
<!--
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-23 18:21:13
|
||||||
|
* @Description: Top bar
|
||||||
|
* @FilePath: /CasaOS-UI/src/components/TopBar.vue
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="navbar top-bar is-flex is-align-items-center">
|
||||||
|
<div class="navbar-brand ml-3">
|
||||||
|
<b-dropdown aria-role="list" class="navbar-item" @active-change="onOpen">
|
||||||
|
<template #trigger>
|
||||||
|
<p role="button">
|
||||||
|
<b-icon pack="fas" icon="sliders-h">
|
||||||
|
</b-icon>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<b-dropdown-item aria-role="menu-item" :focusable="false" custom>
|
||||||
|
<h2 class="title is-4">CasaOS Setting</h2>
|
||||||
|
<div class="is-flex is-align-items-center item">
|
||||||
|
<div class="is-flex is-align-items-center flex1">
|
||||||
|
<b-icon pack="fas" icon="sync-alt" class="mr-1"></b-icon> <b>Update</b>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
v{{updateInfo.current_version}}
|
||||||
|
</div>
|
||||||
|
<!-- <b-field>
|
||||||
|
<b-switch v-model="barData.auto_update" type="is-dark" size="is-small" class="is-flex-direction-row-reverse mr-0" @input="saveData">
|
||||||
|
Auto-Check
|
||||||
|
</b-switch>
|
||||||
|
</b-field> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="is-flex is-align-items-center pl-5" v-if="!updateInfo.is_need">
|
||||||
|
{{latestText}}
|
||||||
|
<b-icon type="is-success" pack="fas" icon="check" class="ml-1"></b-icon>
|
||||||
|
</div>
|
||||||
|
<div class="is-flex is-align-items-center is-justify-content-end update-container pl-5" v-if="updateInfo.is_need">
|
||||||
|
<div class="flex1">{{updateText}}</div>
|
||||||
|
<b-button type="is-dark" size="is-small" class="ml-2" :loading="isUpdating" rounded @click="updateSystem">Update</b-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</b-dropdown-item>
|
||||||
|
</b-dropdown>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-menu">
|
||||||
|
<div class="navbar-end mr-3">
|
||||||
|
<!-- <b-icon pack="far" icon="comment-alt"></b-icon> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "top-bar",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
timer: 0,
|
||||||
|
barData: {
|
||||||
|
auto_update: false,
|
||||||
|
background: "",
|
||||||
|
background_type: "d",
|
||||||
|
search_engine: "google",
|
||||||
|
search_switch: false,
|
||||||
|
shortcuts_switch: false,
|
||||||
|
widgets_switch: false
|
||||||
|
},
|
||||||
|
updateInfo: {
|
||||||
|
current_version: '0',
|
||||||
|
is_need: false,
|
||||||
|
version: Object
|
||||||
|
},
|
||||||
|
isUpdating: false,
|
||||||
|
latestText: "Currently the latest version",
|
||||||
|
updateText: "A new version is available!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getConfig();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* @description: Get CasaOs Configs
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
getConfig() {
|
||||||
|
this.$api.info.systemConfig().then(res => {
|
||||||
|
if (res.data.success == 200) {
|
||||||
|
this.barData = res.data.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Save CasaOs Configs
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
saveData() {
|
||||||
|
this.$api.info.saveSystemConfig(this.barData).then(res => {
|
||||||
|
if (res.data.success == 200) {
|
||||||
|
console.log(res);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Handle Dropmenu state
|
||||||
|
* @param {Boolean} isOpen
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
onOpen(isOpen) {
|
||||||
|
if (isOpen) {
|
||||||
|
this.$api.info.checkVersion().then(res => {
|
||||||
|
if (res.data.success == 200) {
|
||||||
|
this.updateInfo = res.data.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Update System Version and check update state
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
updateSystem() {
|
||||||
|
this.isUpdating = true;
|
||||||
|
this.$api.info.updateSystem().then(res => {
|
||||||
|
if (res.data.success == 200) {
|
||||||
|
console.log(res.data.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.checkUpdateState();
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @description: check update state if is_need is false then reload page
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
checkUpdateState() {
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
this.$api.info.checkVersion().then(res => {
|
||||||
|
if (res.data.success == 200) {
|
||||||
|
if (!res.data.data.is_need) {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
154
UI/src/components/forms/ImportPanel.vue
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal-card">
|
||||||
|
<!-- Modal-Card Header Start -->
|
||||||
|
<header class="modal-card-head">
|
||||||
|
<div class="flex1">
|
||||||
|
<h3 class="title is-4 has-text-weight-normal">Import From Docker CLI</h3>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<!-- Modal-Card Header End -->
|
||||||
|
<!-- Modal-Card Body Start -->
|
||||||
|
<section class="modal-card-body">
|
||||||
|
<b-field label="Command Line" :type="{ 'is-danger': parseError}" :message="errors">
|
||||||
|
<b-input maxlength="800" type="textarea" class="import-area" v-model="dockerCliCommands"></b-input>
|
||||||
|
</b-field>
|
||||||
|
</section>
|
||||||
|
<!-- Modal-Card Body End -->
|
||||||
|
<!-- Modal-Card Footer Start-->
|
||||||
|
<footer class="modal-card-foot is-flex is-align-items-center">
|
||||||
|
<div class="flex1"></div>
|
||||||
|
<div>
|
||||||
|
<b-button label="Cancel" @click="$emit('close')" rounded />
|
||||||
|
<b-button label="Sumbit" type="is-dark" @click="emitSubmit" rounded />
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<!-- Modal-Card Footer End -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import parser from 'yargs-parser'
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dockerCliCommands: "",
|
||||||
|
parseError: false,
|
||||||
|
errors: "",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
initData: Object,
|
||||||
|
netWorks: Array
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
console.log(this.netWorks);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Emit Event to tell parent Update
|
||||||
|
* @param {*}
|
||||||
|
* @return {*} void
|
||||||
|
*/
|
||||||
|
emitSubmit() {
|
||||||
|
if (this.parseCli()) {
|
||||||
|
this.errors = ""
|
||||||
|
this.$emit('update', this.initData)
|
||||||
|
this.$emit('close')
|
||||||
|
} else {
|
||||||
|
this.errors = "Please fill correct command line"
|
||||||
|
this.parseError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Parse Import Docker Cli Commands
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
parseCli() {
|
||||||
|
const formattedInput = this.dockerCliCommands.replace(/\<[^\>]*\>/g, 'Custom_data').replace(/[\r\n]/g, "").replace(/\\/g, "\\ ").trim();
|
||||||
|
const parsedInput = parser(formattedInput)
|
||||||
|
console.log(parsedInput);
|
||||||
|
const { _: command, ...params } = parsedInput;
|
||||||
|
if (command[0] !== 'docker' || (command[1] !== 'run' && command[1] !== 'create')) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
//Envs
|
||||||
|
this.initData.envs = this.makeArray(parsedInput.e).map(item => {
|
||||||
|
let ii = item.split("=");
|
||||||
|
return {
|
||||||
|
container: ii[0],
|
||||||
|
host: ii[1]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//Ports
|
||||||
|
this.initData.ports = this.makeArray(parsedInput.p).map(item => {
|
||||||
|
let pArray = item.split(":")
|
||||||
|
let endArray = pArray[1].split("/")
|
||||||
|
let protocol = (endArray[1]) ? endArray[1] : 'tcp';
|
||||||
|
return {
|
||||||
|
container: endArray[0],
|
||||||
|
host: pArray[0],
|
||||||
|
protocol: protocol
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//Volume
|
||||||
|
this.initData.volumes = this.makeArray(parsedInput.v).map(item => {
|
||||||
|
let ii = item.split(":");
|
||||||
|
return {
|
||||||
|
container: ii[1],
|
||||||
|
host: ii[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Devices
|
||||||
|
this.initData.devices = this.makeArray(parsedInput.device).map(item => {
|
||||||
|
let ii = item.split(":");
|
||||||
|
return {
|
||||||
|
container: ii[1],
|
||||||
|
host: ii[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//Network
|
||||||
|
if (parsedInput.network != undefined) {
|
||||||
|
let network = (parsedInput.network == 'physical') ? 'macvlan' : parsedInput.network;
|
||||||
|
let seletNetworks = this.netWorks.filter(item => {
|
||||||
|
if (item.driver == network) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (seletNetworks.length > 0) {
|
||||||
|
this.initData.network_model = seletNetworks[0].networks[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Image
|
||||||
|
this.initData.image = [...command].pop()
|
||||||
|
//Label
|
||||||
|
if (parsedInput.name != undefined) {
|
||||||
|
this.initData.label = parsedInput.name.replace(/^\S/, s => s.toUpperCase())
|
||||||
|
}
|
||||||
|
//Restart
|
||||||
|
if (parsedInput.restart != undefined) {
|
||||||
|
this.initData.restart = parsedInput.restart
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: Make String to Array
|
||||||
|
* @param {*}
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
|
makeArray(foo) {
|
||||||
|
let newArray = (typeof (foo) == "string") ? [foo] : foo
|
||||||
|
return (newArray == undefined) ? [] : newArray
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
108
UI/src/components/forms/InputGroup.vue
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-5">
|
||||||
|
<div class="field is-flex is-align-items-center mb-2">
|
||||||
|
<label class="label mb-0 flex1">{{label}}</label>
|
||||||
|
<b-button icon-left="plus" type="is-dark" size="is-small" rounded @click="addItem">Add</b-button>
|
||||||
|
</div>
|
||||||
|
<div class="is-flex is-align-items-center mb-5 info" v-if="vdata.length == 0">
|
||||||
|
<b-icon pack="fas" icon="info-circle" size="is-small" class="mr-2 "></b-icon>
|
||||||
|
<span>
|
||||||
|
{{message}}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="port-item" v-for="(item,index) in vdata" :key="'port'+index">
|
||||||
|
<b-icon pack="fas" icon="times" size="is-small" class="is-clickable" @click.native="removeItem(index)"></b-icon>
|
||||||
|
<template v-if="index < 1">
|
||||||
|
<b-field grouped>
|
||||||
|
<b-field :label="name1" expanded>
|
||||||
|
<b-input :placeholder="name1" v-model="item.container" expanded @input="handleInput"></b-input>
|
||||||
|
</b-field>
|
||||||
|
<b-field :label="name2" expanded>
|
||||||
|
<b-input :placeholder="name2" v-model="item.host" expanded @input="handleInput"></b-input>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
</b-field>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
|
||||||
|
<b-field grouped>
|
||||||
|
<b-input :placeholder="name1" v-model="item.container" expanded @input="handleInput"></b-input>
|
||||||
|
<b-input :placeholder="name2" v-model="item.host" expanded @input="handleInput"></b-input>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name:'input-group',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
items: [],
|
||||||
|
min: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
prop: 'vdata',
|
||||||
|
event: 'change'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
vdata: Array,
|
||||||
|
label: String,
|
||||||
|
message: String,
|
||||||
|
name1: {
|
||||||
|
type: String,
|
||||||
|
default: "Container"
|
||||||
|
},
|
||||||
|
name2: {
|
||||||
|
type: String,
|
||||||
|
default: "Host"
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
//this.items = this.vdata;
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
//this.addItem()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addItem() {
|
||||||
|
let itemObj = {
|
||||||
|
container: "",
|
||||||
|
host: ""
|
||||||
|
}
|
||||||
|
this.vdata.push(itemObj)
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem(index) {
|
||||||
|
this.vdata.splice(index, 1)
|
||||||
|
this.filterArray()
|
||||||
|
},
|
||||||
|
handleInput() {
|
||||||
|
this.filterArray()
|
||||||
|
},
|
||||||
|
filterArray() {
|
||||||
|
// let newArray = this.items.filter(item => {
|
||||||
|
// if (item.container != "" && item.host != "") {
|
||||||
|
// return true
|
||||||
|
// } else {
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
this.$emit('change', this.vdata)
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
134
UI/src/components/forms/Ports.vue
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-5">
|
||||||
|
<div class="field is-flex is-align-items-center mb-2">
|
||||||
|
<label class="label mb-0 flex1">Ports</label>
|
||||||
|
<b-button icon-left="plus" type="is-dark" size="is-small" rounded @click="addItem">Add</b-button>
|
||||||
|
</div>
|
||||||
|
<div class="is-flex is-align-items-center mb-5 info" v-if="vdata.length == 0">
|
||||||
|
<b-icon pack="fas" icon="info-circle" size="is-small" class="mr-2 "></b-icon>
|
||||||
|
<span>
|
||||||
|
No App Ports now, Click “+” to add one.
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="port-item" v-for="(item,index) in vdata" :key="'port'+index">
|
||||||
|
<b-icon pack="fas" icon="times" size="is-small" class="is-clickable" @click.native="removeItem(index)"></b-icon>
|
||||||
|
<template v-if="index < 1">
|
||||||
|
<b-field grouped>
|
||||||
|
<b-field label="Container" expanded>
|
||||||
|
<b-input placeholder="Container" type="number" v-model="item.container" expanded @input="handleInput"></b-input>
|
||||||
|
</b-field>
|
||||||
|
<b-field label="Host" expanded>
|
||||||
|
<b-input placeholder="Host" type="number" v-model="item.host" expanded @input="handleInput" v-if="showHostPost"></b-input>
|
||||||
|
</b-field>
|
||||||
|
<b-field label="Protocol" expanded>
|
||||||
|
<b-select placeholder="Protocol" v-model="item.protocol" expanded @input="handleInput">
|
||||||
|
<option value="tcp">TCP</option>
|
||||||
|
<option value="udp">UDP</option>
|
||||||
|
<option value="both">TCP + UDP</option>
|
||||||
|
</b-select>
|
||||||
|
</b-field>
|
||||||
|
</b-field>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
|
||||||
|
<b-field grouped>
|
||||||
|
<b-input placeholder="Container" type="number" v-model="item.container" expanded @input="handleInput"></b-input>
|
||||||
|
<b-input placeholder="Host" type="number" v-model="item.host" expanded @input="handleInput" v-if="showHostPost"></b-input>
|
||||||
|
<b-select placeholder="Protocol" v-model="item.protocol" expanded @input="handleInput">
|
||||||
|
<option value="tcp">TCP</option>
|
||||||
|
<option value="udp">UDP</option>
|
||||||
|
<option value="both">TCP + UDP</option>
|
||||||
|
</b-select>
|
||||||
|
</b-field>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ports',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
items: [],
|
||||||
|
min: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
prop: 'vdata',
|
||||||
|
event: 'change'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
vdata: Array,
|
||||||
|
showHostPost: Boolean
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
//this.items = this.vdata;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.vdata.length == 0) {
|
||||||
|
//this.addItem()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addItem() {
|
||||||
|
let itemObj = {
|
||||||
|
container: "",
|
||||||
|
host: "",
|
||||||
|
protocol: "tcp"
|
||||||
|
}
|
||||||
|
this.vdata.push(itemObj)
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem(index) {
|
||||||
|
this.vdata.splice(index, 1)
|
||||||
|
this.filterArray()
|
||||||
|
},
|
||||||
|
handleInput() {
|
||||||
|
this.filterArray()
|
||||||
|
},
|
||||||
|
filterArray() {
|
||||||
|
// let newArray = this.items.filter(item => {
|
||||||
|
// if (item.container != "" && item.host != "") {
|
||||||
|
// return true
|
||||||
|
// } else {
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
this.$emit('change', this.vdata)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.info {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #5a5a5a;
|
||||||
|
}
|
||||||
|
.port-item {
|
||||||
|
position: relative;
|
||||||
|
.icon {
|
||||||
|
position: absolute;
|
||||||
|
right: -1.5rem;
|
||||||
|
bottom: 0.825rem;
|
||||||
|
}
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.field.is-expanded {
|
||||||
|
.label {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
17
UI/src/main.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import App from '@/App.vue'
|
||||||
|
import router from '@/router'
|
||||||
|
import store from '@/store'
|
||||||
|
import api from '@/service/api.js'
|
||||||
|
import Buefy from 'buefy'
|
||||||
|
import '@/assets/scss/app.scss'
|
||||||
|
|
||||||
|
Vue.use(Buefy)
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
Vue.prototype.$api = api;
|
||||||
|
new Vue({
|
||||||
|
router,
|
||||||
|
store,
|
||||||
|
render: h => h(App)
|
||||||
|
}).$mount('#app')
|
28
UI/src/plugins/vee-validate.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { required, confirmed, length, email, min } from "vee-validate/dist/rules";
|
||||||
|
import { extend } from "vee-validate";
|
||||||
|
|
||||||
|
extend("required", {
|
||||||
|
...required,
|
||||||
|
message: "This field is required"
|
||||||
|
});
|
||||||
|
|
||||||
|
extend("email", {
|
||||||
|
...email,
|
||||||
|
message: "This field must be a valid email"
|
||||||
|
});
|
||||||
|
|
||||||
|
extend("confirmed", {
|
||||||
|
...confirmed,
|
||||||
|
message: "This field confirmation does not match"
|
||||||
|
});
|
||||||
|
|
||||||
|
extend("length", {
|
||||||
|
...length,
|
||||||
|
message: "This field must have 2 options"
|
||||||
|
});
|
||||||
|
|
||||||
|
extend("min", {
|
||||||
|
...min,
|
||||||
|
message: "This field must have more than {length} characters"
|
||||||
|
});
|
||||||
|
|
24
UI/src/router/index.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-18 23:19:27
|
||||||
|
* @Description:
|
||||||
|
* @FilePath: \CasaOS-UI\src\router\index.js
|
||||||
|
*/
|
||||||
|
import Vue from 'vue'
|
||||||
|
import VueRouter from 'vue-router'
|
||||||
|
|
||||||
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = new VueRouter({
|
||||||
|
mode: 'history',
|
||||||
|
base: process.env.BASE_URL,
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
19
UI/src/service/api.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-23 15:59:52
|
||||||
|
* @Description:
|
||||||
|
* @FilePath: /CasaOS-UI/src/service/api.js
|
||||||
|
*/
|
||||||
|
import user from "./user.js";
|
||||||
|
import app from './app.js';
|
||||||
|
import task from './task.js';
|
||||||
|
import info from './info.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
app,
|
||||||
|
info,
|
||||||
|
user,
|
||||||
|
task
|
||||||
|
}
|
79
UI/src/service/app.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-19 09:26:20
|
||||||
|
* @Description: Application API
|
||||||
|
* @FilePath: \CasaOS-UI\src\service\app.js
|
||||||
|
*/
|
||||||
|
import { api } from "./service.js";
|
||||||
|
|
||||||
|
const app = {
|
||||||
|
//Get Install Info
|
||||||
|
appConfig() {
|
||||||
|
return api.get("/app/install/config");
|
||||||
|
},
|
||||||
|
//Store List
|
||||||
|
storeList(data) {
|
||||||
|
return api.get("/app/list", data);
|
||||||
|
},
|
||||||
|
//Store App Info
|
||||||
|
storeAppInfo(id) {
|
||||||
|
return api.get("/app/appinfo/" + id);
|
||||||
|
},
|
||||||
|
//Store Category List
|
||||||
|
storeCategoryList() {
|
||||||
|
return api.get("/app/category");
|
||||||
|
},
|
||||||
|
//Check Port
|
||||||
|
checkPort(port, type) {
|
||||||
|
let data = {
|
||||||
|
type: type
|
||||||
|
}
|
||||||
|
return api.get('/app/check/' + port, data);
|
||||||
|
},
|
||||||
|
// Get a free port
|
||||||
|
getPort() {
|
||||||
|
return api.get('/app/getport');
|
||||||
|
},
|
||||||
|
// Get app Running State
|
||||||
|
getState(id, data) {
|
||||||
|
return api.get('/app/state/' + id, data);
|
||||||
|
},
|
||||||
|
//Install App
|
||||||
|
install(id, data) {
|
||||||
|
return api.post('/app/install/' + id, data);
|
||||||
|
},
|
||||||
|
//Install Info
|
||||||
|
state(id) {
|
||||||
|
return api.get('/app/speed/' + id);
|
||||||
|
},
|
||||||
|
// Uninstall App
|
||||||
|
uninstall(id) {
|
||||||
|
return api.delete('/app/uninstall/' + id);
|
||||||
|
},
|
||||||
|
//My App List
|
||||||
|
myAppList(data) {
|
||||||
|
return api.get('/app/mylist', data);
|
||||||
|
},
|
||||||
|
//Container info
|
||||||
|
getContainerInfo(id) {
|
||||||
|
return api.get('/app/info/' + id);
|
||||||
|
},
|
||||||
|
//Container Log
|
||||||
|
getContainerLogs(id) {
|
||||||
|
return api.get('/app/logs/' + id)
|
||||||
|
},
|
||||||
|
//Start Or Stop Or Restart A Container with ID
|
||||||
|
startContainer(id, data) {
|
||||||
|
return api.put('/app/state/' + id, data)
|
||||||
|
},
|
||||||
|
getContainerSettingdata(id) {
|
||||||
|
return api.get(`/app/update/${id}/info`)
|
||||||
|
},
|
||||||
|
//Update Container Settings
|
||||||
|
updateContainerSetting(id, data) {
|
||||||
|
return api.put(`/app/update/${id}/setting`, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default app;
|
37
UI/src/service/ddns.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-19 09:26:08
|
||||||
|
* @Description: DDNS Service API
|
||||||
|
* @FilePath: \CasaOS-UI\src\service\ddns.js
|
||||||
|
*/
|
||||||
|
import { api } from "./service.js";
|
||||||
|
|
||||||
|
const ddns = {
|
||||||
|
//Add New DDNS
|
||||||
|
add(data) {
|
||||||
|
return api.post("/ddns/set", data);
|
||||||
|
},
|
||||||
|
//Delete a DDNS Item
|
||||||
|
delete(id) {
|
||||||
|
return api.delete("/ddns/delete/" + id);
|
||||||
|
},
|
||||||
|
//Get DDNS List
|
||||||
|
get_list() {
|
||||||
|
return api.get('/ddns/list');
|
||||||
|
},
|
||||||
|
//Ger DDNS Provider List
|
||||||
|
get_provider_list() {
|
||||||
|
return api.get('/ddns/getlist');
|
||||||
|
},
|
||||||
|
//Get Public Internet IP address (IPv4)
|
||||||
|
get_ipv4() {
|
||||||
|
return api.get('/ddns/ip');
|
||||||
|
},
|
||||||
|
// Ping Host
|
||||||
|
ping(host) {
|
||||||
|
return api.get('/ddns/ping/' + host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default ddns;
|
35
UI/src/service/disk.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-19 09:26:02
|
||||||
|
* @Description: Disk API
|
||||||
|
* @FilePath: \CasaOS-UI\src\service\disk.js
|
||||||
|
*/
|
||||||
|
import { api } from "./service.js";
|
||||||
|
|
||||||
|
const disk = {
|
||||||
|
// get Path list
|
||||||
|
diskInfo() {
|
||||||
|
return api.get('/disk/info');
|
||||||
|
},
|
||||||
|
diskList() {
|
||||||
|
return api.get('/disk/list');
|
||||||
|
},
|
||||||
|
// System path
|
||||||
|
renamePath(oldpath, path) {
|
||||||
|
let data = {
|
||||||
|
oldpath: oldpath,
|
||||||
|
newpath: path
|
||||||
|
}
|
||||||
|
return api.get('/zima/rename', data);
|
||||||
|
},
|
||||||
|
// Make a new Dir
|
||||||
|
mkdir(path) {
|
||||||
|
let data = {
|
||||||
|
path: path
|
||||||
|
}
|
||||||
|
return api.get('/zima/mkdir', data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default disk;
|
36
UI/src/service/file.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-19 09:25:53
|
||||||
|
* @Description: File API
|
||||||
|
* @FilePath: \CasaOS-UI\src\service\file.js
|
||||||
|
*/
|
||||||
|
import { api } from "./service.js";
|
||||||
|
|
||||||
|
const file = {
|
||||||
|
// get Path list
|
||||||
|
dirPath(path) {
|
||||||
|
let data = {
|
||||||
|
path: path
|
||||||
|
}
|
||||||
|
return api.get('/file/dirpath', data);
|
||||||
|
},
|
||||||
|
// System path
|
||||||
|
renamePath(oldpath, path) {
|
||||||
|
let data = {
|
||||||
|
oldpath: oldpath,
|
||||||
|
newpath: path
|
||||||
|
}
|
||||||
|
return api.get('/file/rename', data);
|
||||||
|
},
|
||||||
|
// Make a new Dir
|
||||||
|
mkdir(path) {
|
||||||
|
let data = {
|
||||||
|
path: path
|
||||||
|
}
|
||||||
|
return api.post('/file/mkdir', data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default file;
|
53
UI/src/service/info.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-23 17:41:01
|
||||||
|
* @Description: System HardWare Info API
|
||||||
|
* @FilePath: /CasaOS-UI/src/service/info.js
|
||||||
|
*/
|
||||||
|
import { api } from "./service.js";
|
||||||
|
|
||||||
|
const info = {
|
||||||
|
//CPU info
|
||||||
|
cpuInfo() {
|
||||||
|
return api.get("/zima/getcpuinfo");
|
||||||
|
},
|
||||||
|
//Memory Info
|
||||||
|
memoryInfo() {
|
||||||
|
return api.get("/zima/getmeminfo");
|
||||||
|
},
|
||||||
|
//Network Info
|
||||||
|
networkInfo() {
|
||||||
|
return api.get('/zima/getnetinfo');
|
||||||
|
},
|
||||||
|
//Disk Info
|
||||||
|
diskInfo() {
|
||||||
|
return api.get('/zima/getdiskinfo');
|
||||||
|
},
|
||||||
|
//All Info
|
||||||
|
allInfo() {
|
||||||
|
return api.get('/zima/getinfo');
|
||||||
|
},
|
||||||
|
// System Info
|
||||||
|
systemInfo() {
|
||||||
|
return api.get('/zima/sysinfo');
|
||||||
|
},
|
||||||
|
//Get CasaOS Config
|
||||||
|
systemConfig() {
|
||||||
|
return api.get('/sys/config')
|
||||||
|
},
|
||||||
|
//Save CasaOs Config
|
||||||
|
saveSystemConfig(data) {
|
||||||
|
return api.post('/sys/config', data)
|
||||||
|
},
|
||||||
|
// Check Verison
|
||||||
|
checkVersion() {
|
||||||
|
return api.get('/sys/check');
|
||||||
|
},
|
||||||
|
//Update System
|
||||||
|
updateSystem(){
|
||||||
|
return api.post('/sys/update');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default info;
|
138
UI/src/service/service.js
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-23 17:26:31
|
||||||
|
* @Description:
|
||||||
|
* @FilePath: /CasaOS-UI/src/service/service.js
|
||||||
|
*/
|
||||||
|
import axios from 'axios'
|
||||||
|
import qs from 'qs'
|
||||||
|
import router from '@/router'
|
||||||
|
import store from '@/store'
|
||||||
|
// Set Post Headers
|
||||||
|
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
|
||||||
|
axios.defaults.withCredentials = false;
|
||||||
|
if (process.env.NODE_ENV === "'dev'") {
|
||||||
|
axios.defaults.baseURL = `http://${store.state.devIp}:8089/v1`;
|
||||||
|
} else {
|
||||||
|
axios.defaults.baseURL = `${document.location.protocol}//${document.location.host}/v1`
|
||||||
|
}
|
||||||
|
|
||||||
|
//Create a axios instance, And set timeout to 30s
|
||||||
|
const instance = axios.create({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
window.isRefreshing = false
|
||||||
|
|
||||||
|
let refreshSubscribers = []
|
||||||
|
|
||||||
|
function subscribeTokenRefresh(cb) {
|
||||||
|
refreshSubscribers.push(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRrefreshed(token) {
|
||||||
|
refreshSubscribers.map(cb => cb(token))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request interceptors
|
||||||
|
instance.interceptors.request.use((config) => {
|
||||||
|
let token = ''
|
||||||
|
if (sessionStorage.getItem("user_token")) {
|
||||||
|
token = sessionStorage.getItem("user_token")
|
||||||
|
}
|
||||||
|
if (localStorage.getItem("user_token")) {
|
||||||
|
token = localStorage.getItem("user_token")
|
||||||
|
}
|
||||||
|
config.headers.Authorization = token
|
||||||
|
if (token === "" && config.url !== "user/login") {
|
||||||
|
if (!window.isRefreshing) {
|
||||||
|
window.isRefreshing = true;
|
||||||
|
axios.post('user/login', qs.stringify({
|
||||||
|
username: "admin",
|
||||||
|
pwd: "admin"
|
||||||
|
})).then(res => {
|
||||||
|
token = res.data.data;
|
||||||
|
store.commit('setToken', token)
|
||||||
|
localStorage.setItem("user_token", token)
|
||||||
|
onRrefreshed(token);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let retry = new Promise((resolve) => {
|
||||||
|
/* (token) => {...}这个函数就是回调函数 */
|
||||||
|
subscribeTokenRefresh((token) => {
|
||||||
|
config.headers.Authorization = token
|
||||||
|
/* 将请求挂起 */
|
||||||
|
resolve(config)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return retry
|
||||||
|
} else {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}, (error) => {
|
||||||
|
// Do something with request error
|
||||||
|
return Promise.reject(error)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 响应拦截(请求返回后拦截)
|
||||||
|
instance.interceptors.response.use(response => {
|
||||||
|
//console.log("响应拦截", response);
|
||||||
|
return response;
|
||||||
|
}, error => {
|
||||||
|
console.log('catch', error)
|
||||||
|
if (error.response) {
|
||||||
|
|
||||||
|
switch (error.response.status) {
|
||||||
|
case 401:
|
||||||
|
sessionStorage.removeItem('user_token') //可能是token过期,清除它
|
||||||
|
router.replace({ //跳转到登录页面
|
||||||
|
path: '/',
|
||||||
|
query: { redirect: router.currentRoute.fullPath } // 将跳转的路由path作为参数,登录成功后跳转到该路由
|
||||||
|
})
|
||||||
|
break;
|
||||||
|
case 404:
|
||||||
|
store.commit('setServiceError', true);
|
||||||
|
break;
|
||||||
|
case 500:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
store.commit('setServiceError', true);
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
})
|
||||||
|
|
||||||
|
//按照请求类型对axios进行封装
|
||||||
|
const api = {
|
||||||
|
get(url, data) {
|
||||||
|
return instance.get(url, { params: data })
|
||||||
|
},
|
||||||
|
post(url, data) {
|
||||||
|
let newData = (url.indexOf("install") > 0 || url.indexOf("sys") > 0) ? JSON.stringify(data) : qs.stringify(data)
|
||||||
|
if (url.indexOf("install") > 0) {
|
||||||
|
axios.defaults.headers.post['Content-Type'] = 'application/json';
|
||||||
|
} else {
|
||||||
|
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
|
||||||
|
}
|
||||||
|
return instance.post(url, newData)
|
||||||
|
},
|
||||||
|
put(url, data) {
|
||||||
|
let newData = (url.indexOf("setting") > 0) ? JSON.stringify(data) : qs.stringify(data)
|
||||||
|
if (url.indexOf("setting") > 0) {
|
||||||
|
axios.defaults.headers.post['Content-Type'] = 'application/json';
|
||||||
|
} else {
|
||||||
|
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
|
||||||
|
}
|
||||||
|
return instance.put(url, newData)
|
||||||
|
},
|
||||||
|
delete(url, data) {
|
||||||
|
return instance.delete(url, { params: data })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export { api }
|
21
UI/src/service/task.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-19 09:26:45
|
||||||
|
* @Description: Task API
|
||||||
|
* @FilePath: \CasaOS-UI\src\service\task.js
|
||||||
|
*/
|
||||||
|
import { api } from "./service.js";
|
||||||
|
|
||||||
|
const task = {
|
||||||
|
//List
|
||||||
|
list() {
|
||||||
|
return api.get("/task/list");
|
||||||
|
},
|
||||||
|
//Mark
|
||||||
|
completion(id) {
|
||||||
|
return api.put(`/task/completion/${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default task;
|
47
UI/src/service/user.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-19 09:26:47
|
||||||
|
* @Description: User API
|
||||||
|
* @FilePath: \CasaOS-UI\src\service\user.js
|
||||||
|
*/
|
||||||
|
import { api } from "./service.js";
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
//login
|
||||||
|
login(data) {
|
||||||
|
return api.post("user/login", data);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Create UserName and Password
|
||||||
|
createUsernameAndPaword(data) {
|
||||||
|
return api.post("/user/setusernamepwd", data);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Change User Avatar
|
||||||
|
changeAvatar(data) {
|
||||||
|
return api.post("/user/changhead", data);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Change UserName
|
||||||
|
changeUserName(data) {
|
||||||
|
return api.put("/user/changusername", data);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Change User Password
|
||||||
|
changePassword(data) {
|
||||||
|
return api.put("/user/changuserpwd", data);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get user info
|
||||||
|
getUserInfo() {
|
||||||
|
return api.get("/user/info");
|
||||||
|
},
|
||||||
|
|
||||||
|
// Change User Info
|
||||||
|
changeUserInfo(data) {
|
||||||
|
return api.post('/user/changuserinfo', data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default user;
|
64
UI/src/service/zerotier.js
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-19 09:26:50
|
||||||
|
* @Description: Zerotier API
|
||||||
|
* @FilePath: \CasaOS-UI\src\service\zerotier.js
|
||||||
|
*/
|
||||||
|
import { api } from "./service.js";
|
||||||
|
|
||||||
|
const zerotier = {
|
||||||
|
//Check if Need login to zerotier
|
||||||
|
isLogin() {
|
||||||
|
return api.get("/zerotier/islogin");
|
||||||
|
},
|
||||||
|
//Login
|
||||||
|
login(data) {
|
||||||
|
return api.post("/zerotier/login", data);
|
||||||
|
},
|
||||||
|
//Register
|
||||||
|
register(data) {
|
||||||
|
return api.post('/zerotier/register', data);
|
||||||
|
},
|
||||||
|
//networklist
|
||||||
|
networkLits() {
|
||||||
|
return api.get('/zerotier/list');
|
||||||
|
},
|
||||||
|
//joinNetwork
|
||||||
|
joinNetwork(id) {
|
||||||
|
return api.post(`/zerotier/join/${id}`);
|
||||||
|
},
|
||||||
|
// leaveNetwork
|
||||||
|
leaveNetwork(id) {
|
||||||
|
return api.post(`/zerotier/leave/${id}`);
|
||||||
|
},
|
||||||
|
// Get Network detial
|
||||||
|
networkDetail(id) {
|
||||||
|
return api.get(`/zerotier/info/${id}`);
|
||||||
|
},
|
||||||
|
// Edit Network
|
||||||
|
editNetwork(id, data) {
|
||||||
|
return api.put(`/zerotier/edit/${id}`, data)
|
||||||
|
},
|
||||||
|
// Delete A Network
|
||||||
|
delNetwork(id) {
|
||||||
|
return api.delete(`/zerotier/network/${id}/del`)
|
||||||
|
},
|
||||||
|
createNetwork() {
|
||||||
|
return api.post('/zerotier/create')
|
||||||
|
},
|
||||||
|
// Get Network member list
|
||||||
|
getMembers(id) {
|
||||||
|
return api.get(`/zerotier/member/${id}`)
|
||||||
|
},
|
||||||
|
// Edit Member
|
||||||
|
editMember(id, mId, data) {
|
||||||
|
return api.put(`/zerotier/member/${id}/edit/${mId}`, data)
|
||||||
|
},
|
||||||
|
// Delete Member
|
||||||
|
delMemeber(id, mId) {
|
||||||
|
return api.delete(`/zerotier/member/${id}/del/${mId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default zerotier;
|
34
UI/src/store/index.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-18 21:32:13
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-22 16:28:16
|
||||||
|
* @Description:
|
||||||
|
* @FilePath: /CasaOS-UI/src/store/index.js
|
||||||
|
*/
|
||||||
|
import Vue from 'vue'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
//import createPersistedState from "vuex-persistedstate";
|
||||||
|
|
||||||
|
Vue.use(Vuex)
|
||||||
|
|
||||||
|
export default new Vuex.Store({
|
||||||
|
//plugins: [createPersistedState()],
|
||||||
|
state: {
|
||||||
|
token: "",
|
||||||
|
devIp: "192.168.2.217",
|
||||||
|
serviceError: false
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setToken(state, val) {
|
||||||
|
state.token = val
|
||||||
|
},
|
||||||
|
setServiceError(state, val) {
|
||||||
|
state.serviceError = val
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
},
|
||||||
|
modules: {
|
||||||
|
}
|
||||||
|
})
|
23
UI/vue.config.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* @Author: JerryK
|
||||||
|
* @Date: 2021-09-22 10:10:10
|
||||||
|
* @LastEditors: JerryK
|
||||||
|
* @LastEditTime: 2021-09-22 15:26:47
|
||||||
|
* @Description:
|
||||||
|
* @FilePath: /CasaOS-UI/vue.config.js
|
||||||
|
*/
|
||||||
|
const webpack = require('webpack')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
publicPath: '/ui/',
|
||||||
|
runtimeCompiler: true,
|
||||||
|
lintOnSave: false,
|
||||||
|
productionSourceMap: false,
|
||||||
|
pluginOptions: {
|
||||||
|
|
||||||
|
},
|
||||||
|
chainWebpack: config => {
|
||||||
|
config.plugin('ignore')
|
||||||
|
.use(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/));
|
||||||
|
}
|
||||||
|
}
|
9172
UI/yarn.lock
Normal file
49
conf/conf.ini
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
[app]
|
||||||
|
PAGE_SIZE = 10
|
||||||
|
RuntimeRootPath = runtime/
|
||||||
|
LogSavePath = /casaOS/logs/server/
|
||||||
|
LogSaveName = log
|
||||||
|
LogFileExt = log
|
||||||
|
; 必须的格式
|
||||||
|
DateStrFormat = 20060102
|
||||||
|
DateTimeFormat = 2006-01-02 15:04:05
|
||||||
|
TimeFormat = 15:04:05
|
||||||
|
DateFormat = 2006-01-02
|
||||||
|
|
||||||
|
|
||||||
|
[server]
|
||||||
|
HttpPort = 8089
|
||||||
|
RunMode = debug
|
||||||
|
;ServerApi = http://113.52.135.30:8090
|
||||||
|
;ServerApi = https://casaos.zimaboard.com
|
||||||
|
;ServerApi = http://192.168.2.167:8090
|
||||||
|
ServerApi = http://192.168.2.142:8090
|
||||||
|
|
||||||
|
[user]
|
||||||
|
UserName = admin
|
||||||
|
PWD = zimaboard
|
||||||
|
Email = aaa@222.ddd
|
||||||
|
Description = ddddddd
|
||||||
|
Token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImVyZXJlIiwicGFzc3dvcmQiOiJhZHNmZGYiLCJleHAiOjE2MjQwMDU0ODEsImlzcyI6Imdpbi1ibG9nIn0.JNsCccZuFCwlSMLJg62iOIB2xymk_k7xGa11xhZ07bc
|
||||||
|
|
||||||
|
[zerotier]
|
||||||
|
UserName = ddddd
|
||||||
|
PWD =
|
||||||
|
Token = yBKYyavr2RdFAIVN7iTpzlsB1o6CqTgm
|
||||||
|
|
||||||
|
[redis]
|
||||||
|
Host = 192.168.2.167:6379
|
||||||
|
Password =
|
||||||
|
MaxIdle = 30
|
||||||
|
MaxActive = 30
|
||||||
|
IdleTimeout = 200
|
||||||
|
|
||||||
|
[system]
|
||||||
|
AutoUpdate = true
|
||||||
|
SearchSwitch = true
|
||||||
|
WidgetsSwitch = false
|
||||||
|
ShortcutsSwitch = true
|
||||||
|
SearchEngine = baidu
|
||||||
|
Background = http://baidu.com1
|
||||||
|
BackgroundType = d
|
||||||
|
|
2901
docs/docs.go
Normal file
2839
docs/swagger.json
Normal file
1753
docs/swagger.yaml
Normal file
BIN
file/lll.png
Normal file
After Width: | Height: | Size: 116 KiB |
57
go.mod
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
module oasis
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/PuerkitoBio/goquery v1.7.0
|
||||||
|
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect
|
||||||
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
|
||||||
|
github.com/containerd/containerd v1.5.2
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
|
github.com/docker/docker v20.10.7+incompatible
|
||||||
|
github.com/docker/go-connections v0.4.0
|
||||||
|
github.com/forease/gotld v0.0.0-20190808124948-c50ff635576b
|
||||||
|
github.com/gin-contrib/gzip v0.0.2 // indirect
|
||||||
|
github.com/gin-gonic/gin v1.7.2
|
||||||
|
github.com/go-ini/ini v1.62.0
|
||||||
|
github.com/go-ole/go-ole v1.2.5 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||||
|
github.com/go-openapi/spec v0.20.3 // indirect
|
||||||
|
github.com/go-openapi/swag v0.19.15 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.6.1 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
|
github.com/gomodule/redigo v1.8.5
|
||||||
|
github.com/google/go-github/v36 v36.0.0
|
||||||
|
github.com/gorilla/mux v1.8.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.4.2
|
||||||
|
github.com/jinzhu/copier v0.3.2
|
||||||
|
github.com/json-iterator/go v1.1.11 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||||
|
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||||
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/prestonTao/upnp v0.0.0-20150206124352-f4370df5e109
|
||||||
|
github.com/robfig/cron v1.2.0
|
||||||
|
github.com/satori/go.uuid v1.2.0
|
||||||
|
github.com/shirou/gopsutil/v3 v3.21.5
|
||||||
|
github.com/sirupsen/logrus v1.8.1
|
||||||
|
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||||
|
github.com/swaggo/gin-swagger v1.3.0
|
||||||
|
github.com/swaggo/swag v1.7.0
|
||||||
|
github.com/tidwall/gjson v1.8.0
|
||||||
|
github.com/tklauser/go-sysconf v0.3.6 // indirect
|
||||||
|
github.com/ugorji/go v1.2.6 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
|
||||||
|
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
|
||||||
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||||
|
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect
|
||||||
|
golang.org/x/tools v0.1.3 // indirect
|
||||||
|
google.golang.org/grpc v1.39.0 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||||
|
gorm.io/driver/mysql v1.1.1 // indirect
|
||||||
|
gorm.io/driver/sqlite v1.1.5
|
||||||
|
gorm.io/gorm v1.21.15
|
||||||
|
src.techknowlogick.com/xgo v1.4.1-0.20210909190026-ce016894db20 // indirect
|
||||||
|
)
|
70
main.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/robfig/cron"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"net/http"
|
||||||
|
"oasis/pkg/config"
|
||||||
|
"oasis/pkg/sqlite"
|
||||||
|
loger2 "oasis/pkg/utils/loger"
|
||||||
|
"oasis/route"
|
||||||
|
"oasis/service"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var sqliteDB *gorm.DB
|
||||||
|
|
||||||
|
var swagHandler gin.HandlerFunc
|
||||||
|
var configFlag = flag.String("c", "", "config address")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.Parse()
|
||||||
|
config.InitSetup(*configFlag)
|
||||||
|
loger2.LogSetup()
|
||||||
|
sqliteDB = sqlite.GetDb(config.AppInfo.ProjectPath)
|
||||||
|
//gredis.GetRedisConn(config.RedisInfo),
|
||||||
|
service.MyService = service.NewService(sqliteDB, loger2.NewOLoger())
|
||||||
|
}
|
||||||
|
|
||||||
|
// @title Oasis API
|
||||||
|
// @version 1.0.0
|
||||||
|
// @contact.name lauren.pan
|
||||||
|
// @contact.url https://www.zimaboard.com
|
||||||
|
// @contact.email lauren.pan@icewhale.org
|
||||||
|
// @description Oasis v1版本api
|
||||||
|
// @host 192.168.2.114:8089
|
||||||
|
// @securityDefinitions.apikey ApiKeyAuth
|
||||||
|
// @in header
|
||||||
|
// @name Authorization
|
||||||
|
// @BasePath /v1
|
||||||
|
func main() {
|
||||||
|
//model.Setup()
|
||||||
|
//gredis.Setup()
|
||||||
|
r := route.InitRouter(swagHandler)
|
||||||
|
service.SyncTask(sqliteDB)
|
||||||
|
cron2 := cron.New() //创建一个cron实例
|
||||||
|
//执行定时任务(每5秒执行一次)
|
||||||
|
err := cron2.AddFunc("0 0 0 1/1 * *", func() {
|
||||||
|
//service.UpdataDDNSList(mysqldb)
|
||||||
|
service.SyncTask(sqliteDB)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//启动/关闭
|
||||||
|
cron2.Start()
|
||||||
|
defer cron2.Stop()
|
||||||
|
s := &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%v", config.ServerInfo.HttpPort),
|
||||||
|
Handler: r,
|
||||||
|
ReadTimeout: 60 * time.Second,
|
||||||
|
WriteTimeout: 60 * time.Second,
|
||||||
|
MaxHeaderBytes: 1 << 20,
|
||||||
|
}
|
||||||
|
s.ListenAndServe()
|
||||||
|
|
||||||
|
}
|
43
middleware/gin.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Cors() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
method := c.Request.Method
|
||||||
|
//origin := c.Request.Header.Get("Origin") //请求头部
|
||||||
|
//if origin != "" {
|
||||||
|
//接收客户端发送的origin (重要!)
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
c.Header("Access-Control-Allow-Origin", "*")
|
||||||
|
//服务器支持的所有跨域请求的方法
|
||||||
|
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE")
|
||||||
|
//允许跨域设置可以返回其他子段,可以自定义字段
|
||||||
|
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session")
|
||||||
|
// 允许浏览器(客户端)可以解析的头部 (重要)
|
||||||
|
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
|
||||||
|
//设置缓存时间
|
||||||
|
c.Header("Access-Control-Max-Age", "172800")
|
||||||
|
//允许客户端传递校验信息比如 cookie (重要)
|
||||||
|
c.Header("Access-Control-Allow-Credentials", "true")
|
||||||
|
c.Set("content-type", "application/json")
|
||||||
|
//}
|
||||||
|
|
||||||
|
//允许类型校验
|
||||||
|
if method == "OPTIONS" {
|
||||||
|
c.JSON(http.StatusOK, "ok!")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
100
model/app.go
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerAppList struct {
|
||||||
|
Id uint `gorm:"column:id;primary_key" json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Tagline string `json:"tagline"`
|
||||||
|
Tags Strings `gorm:"type:json" json:"tags"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
ScreenshotLink Strings `gorm:"type:json" json:"screenshot_link"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
TcpPort uint `json:"tcp_port"`
|
||||||
|
PortMap uint `json:"port_map"`
|
||||||
|
ImageVersion string `json:"image_version"`
|
||||||
|
Tip string `json:"tip"`
|
||||||
|
Configures configures `gorm:"type:json" json:"configures"`
|
||||||
|
NetworkModel string `json:"network_mode"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Index string `json:"index"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Author string `json:"author"`
|
||||||
|
MinMemory int `json:"min_memory"`
|
||||||
|
MinDisk int `json:"min_disk"`
|
||||||
|
MaxMemory uint64 `json:"max_memory"`
|
||||||
|
Thumbnail string `json:"thumbnail"`
|
||||||
|
Healthy string `json:"healthy"`
|
||||||
|
Plugins Strings `json:"plugins"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ports struct {
|
||||||
|
ContainerPort uint `json:"container_port"`
|
||||||
|
CommendPort int `json:"commend_port"`
|
||||||
|
Desc string `json:"desc"`
|
||||||
|
Type int `json:"type"` // 1:必选 2:可选 3:默认值不必显示 4:系统处理 5:container内容也可编辑
|
||||||
|
}
|
||||||
|
|
||||||
|
type Volume struct {
|
||||||
|
ContainerPath string `json:"container_path"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Desc string `json:"desc"`
|
||||||
|
Type int `json:"type"` // 1:必选 2:可选 3:默认值不必显示 4:系统处理 5:container内容也可编辑
|
||||||
|
}
|
||||||
|
|
||||||
|
type Envs struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Desc string `json:"desc"`
|
||||||
|
Type int `json:"type"` // 1:必选 2:可选 3:默认值不必显示 4:系统处理 5:container内容也可编辑
|
||||||
|
}
|
||||||
|
|
||||||
|
type Devices struct {
|
||||||
|
ContainerPath string `json:"container_path"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Desc string `json:"desc"`
|
||||||
|
Type int `json:"type"` // 1:必选 2:可选 3:默认值不必显示 4:系统处理 5:container内容也可编辑
|
||||||
|
}
|
||||||
|
|
||||||
|
type configures struct {
|
||||||
|
TcpPorts []Ports `json:"tcp_ports"`
|
||||||
|
UdpPorts []Ports `json:"udp_ports"`
|
||||||
|
Envs []Envs `json:"envs"`
|
||||||
|
Volumes []Volume `json:"volumes"`
|
||||||
|
Devices []Devices `json:"devices"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/****************使gorm支持[]string结构*******************/
|
||||||
|
type Strings []string
|
||||||
|
|
||||||
|
func (c Strings) Value() (driver.Value, error) {
|
||||||
|
b, err := json.Marshal(c)
|
||||||
|
return string(b), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Strings) Scan(input interface{}) error {
|
||||||
|
return json.Unmarshal(input.([]byte), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
/****************使gorm支持[]string结构*******************/
|
||||||
|
|
||||||
|
/****************使gorm支持[]string结构*******************/
|
||||||
|
type MapStrings []map[string]string
|
||||||
|
|
||||||
|
func (c MapStrings) Value() (driver.Value, error) {
|
||||||
|
b, err := json.Marshal(c)
|
||||||
|
return string(b), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MapStrings) Scan(input interface{}) error {
|
||||||
|
return json.Unmarshal(input.([]byte), c)
|
||||||
|
}
|
||||||
|
|
||||||
|
/****************使gorm支持[]string结构*******************/
|
10
model/category.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type ServerCategoryList struct {
|
||||||
|
Id uint `gorm:"column:id;primary_key" json:"id"`
|
||||||
|
//CreatedAt time.Time `json:"created_at"`
|
||||||
|
//
|
||||||
|
//UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Count uint `json:"count"`
|
||||||
|
}
|
9
model/ddns.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type GoDaddyModel struct {
|
||||||
|
Type uint `json:"type"`
|
||||||
|
ApiHost string `json:"api_host"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
}
|
31
model/disk.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type LSBLKModel struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
FsType string `json:"fstype"`
|
||||||
|
Size uint64 `json:"size"`
|
||||||
|
FSSize string `json:"fssize"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Model string `json:"model"` //设备标识符
|
||||||
|
RM bool `json:"rm"` //是否为可移动设备
|
||||||
|
RO bool `json:"ro"` //是否为只读设备
|
||||||
|
State string `json:"state"`
|
||||||
|
PhySec int `json:"phy-sec"` //物理扇区大小
|
||||||
|
Type string `json:"type"`
|
||||||
|
Vendor string `json:"vendor"` //供应商
|
||||||
|
Rev string `json:"rev"` //修订版本
|
||||||
|
FSAvail string `json:"fsavail"` //可用空间
|
||||||
|
FSUse string `json:"fsuse%"` //已用百分比
|
||||||
|
MountPoint string `json:"mountpoint"`
|
||||||
|
Format string `json:"format"`
|
||||||
|
Health string `json:"health"`
|
||||||
|
HotPlug bool `json:"hotplug"`
|
||||||
|
FSUsed string `json:"fsused"`
|
||||||
|
Tran string `json:"tran"`
|
||||||
|
MinIO uint64 `json:"min-io"`
|
||||||
|
UsedPercent float64 `json:"used_percent"`
|
||||||
|
Children []LSBLKModel `json:"children"`
|
||||||
|
//详情特有
|
||||||
|
StartSector uint64 `json:"start_sector,omitempty"`
|
||||||
|
EndSector uint64 `json:"end_sector,omitempty"`
|
||||||
|
}
|
119
model/manifest.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TcpPorts struct {
|
||||||
|
Desc string `json:"desc"`
|
||||||
|
ContainerPort int `json:"container_port"`
|
||||||
|
}
|
||||||
|
type UdpPorts struct {
|
||||||
|
Desc string `json:"desc"`
|
||||||
|
ContainerPort int `json:"container_port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************使用gorm支持json************************************/
|
||||||
|
|
||||||
|
type PortMap struct {
|
||||||
|
ContainerPort string `json:"container,omitempty"`
|
||||||
|
CommendPort string `json:"host,omitempty"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PortArrey []PortMap
|
||||||
|
|
||||||
|
// Value 实现方法
|
||||||
|
func (p PortArrey) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan 实现方法
|
||||||
|
func (p *PortArrey) Scan(input interface{}) error {
|
||||||
|
return json.Unmarshal(input.([]byte), p)
|
||||||
|
}
|
||||||
|
|
||||||
|
/************************************************************************/
|
||||||
|
|
||||||
|
/*******************使用gorm支持json************************************/
|
||||||
|
|
||||||
|
type Env struct {
|
||||||
|
Name string `json:"container"`
|
||||||
|
Value string `json:"host"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSON json.RawMessage
|
||||||
|
|
||||||
|
type EnvArrey []Env
|
||||||
|
|
||||||
|
// Value 实现方法
|
||||||
|
func (p EnvArrey) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(p)
|
||||||
|
//return .MarshalJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan 实现方法
|
||||||
|
func (p *EnvArrey) Scan(input interface{}) error {
|
||||||
|
return json.Unmarshal(input.([]byte), p)
|
||||||
|
}
|
||||||
|
|
||||||
|
/************************************************************************/
|
||||||
|
|
||||||
|
/*******************使用gorm支持json************************************/
|
||||||
|
|
||||||
|
type PathMap struct {
|
||||||
|
ContainerPath string `json:"container"`
|
||||||
|
Path string `json:"host"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PathArrey []PathMap
|
||||||
|
|
||||||
|
// Value 实现方法
|
||||||
|
func (p PathArrey) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan 实现方法
|
||||||
|
func (p *PathArrey) Scan(input interface{}) error {
|
||||||
|
return json.Unmarshal(input.([]byte), p)
|
||||||
|
}
|
||||||
|
|
||||||
|
/************************************************************************/
|
||||||
|
|
||||||
|
//type PostData struct {
|
||||||
|
// Envs EnvArrey `json:"envs,omitempty"`
|
||||||
|
// Udp PortArrey `json:"udp_ports"`
|
||||||
|
// Tcp PortArrey `json:"tcp_ports"`
|
||||||
|
// Volumes PathArrey `json:"volumes"`
|
||||||
|
// Devices PathArrey `json:"devices"`
|
||||||
|
// Port string `json:"port,omitempty"`
|
||||||
|
// PortMap string `json:"port_map"`
|
||||||
|
// CpuShares int64 `json:"cpu_shares,omitempty"`
|
||||||
|
// Memory int64 `json:"memory,omitempty"`
|
||||||
|
// Restart string `json:"restart,omitempty"`
|
||||||
|
// EnableUPNP bool `json:"enable_upnp"`
|
||||||
|
// Label string `json:"label"`
|
||||||
|
// Position bool `json:"position"`
|
||||||
|
//}
|
||||||
|
|
||||||
|
type CustomizationPostData struct {
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
NetworkModel string `json:"network_model"`
|
||||||
|
Index string `json:"index"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Envs EnvArrey `json:"envs"`
|
||||||
|
Ports PortArrey `json:"ports"`
|
||||||
|
Volumes PathArrey `json:"volumes"`
|
||||||
|
Devices PathArrey `json:"devices"`
|
||||||
|
//Port string `json:"port,omitempty"`
|
||||||
|
PortMap string `json:"port_map"`
|
||||||
|
CpuShares int64 `json:"cpu_shares"`
|
||||||
|
Memory int64 `json:"memory"`
|
||||||
|
Restart string `json:"restart"`
|
||||||
|
EnableUPNP bool `json:"enable_upnp"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Position bool `json:"position"`
|
||||||
|
}
|
19
model/net.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type IOCountersStat struct {
|
||||||
|
Name string `json:"name"` // interface name
|
||||||
|
BytesSent uint64 `json:"bytesSent"` // number of bytes sent
|
||||||
|
BytesRecv uint64 `json:"bytesRecv"` // number of bytes received
|
||||||
|
PacketsSent uint64 `json:"packetsSent"` // number of packets sent
|
||||||
|
PacketsRecv uint64 `json:"packetsRecv"` // number of packets received
|
||||||
|
Errin uint64 `json:"errin"` // total number of errors while receiving
|
||||||
|
Errout uint64 `json:"errout"` // total number of errors while sending
|
||||||
|
Dropin uint64 `json:"dropin"` // total number of incoming packets which were dropped
|
||||||
|
Dropout uint64 `json:"dropout"` // total number of outgoing packets which were dropped (always 0 on OSX and BSD)
|
||||||
|
Fifoin uint64 `json:"fifoin"` // total number of FIFO buffers errors while receiving
|
||||||
|
Fifoout uint64 `json:"fifoout"` // total number of FIFO buffers errors while sending
|
||||||
|
State string `json:"state"`
|
||||||
|
DateTime time.Time `json:"date_time"`
|
||||||
|
}
|
70
model/sys_common.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
//系统配置
|
||||||
|
type SysInfoModel struct {
|
||||||
|
Name string //系统名称
|
||||||
|
}
|
||||||
|
|
||||||
|
//用户相关
|
||||||
|
type UserModel struct {
|
||||||
|
UserName string
|
||||||
|
PWD string
|
||||||
|
Token string
|
||||||
|
Head string
|
||||||
|
Email string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
//服务配置
|
||||||
|
type ServerModel struct {
|
||||||
|
HttpPort string
|
||||||
|
RunMode string
|
||||||
|
ServerApi string
|
||||||
|
}
|
||||||
|
|
||||||
|
//服务配置
|
||||||
|
type APPModel struct {
|
||||||
|
LogSavePath string
|
||||||
|
LogSaveName string
|
||||||
|
LogFileExt string
|
||||||
|
DateStrFormat string
|
||||||
|
DateTimeFormat string
|
||||||
|
TimeFormat string
|
||||||
|
DateFormat string
|
||||||
|
ProjectPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
//公共返回模型
|
||||||
|
type Result struct {
|
||||||
|
Success int `json:"success" example:"200"`
|
||||||
|
Message string `json:"message" example:"ok"`
|
||||||
|
Data interface{} `json:"data" example:"返回结果"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//zeritier相关
|
||||||
|
type ZeroTierModel struct {
|
||||||
|
UserName string
|
||||||
|
PWD string
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
|
||||||
|
//redis配置文件
|
||||||
|
type RedisModel struct {
|
||||||
|
Host string
|
||||||
|
Password string
|
||||||
|
MaxIdle int
|
||||||
|
MaxActive int
|
||||||
|
IdleTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type SystemConfig struct {
|
||||||
|
SearchSwitch bool `json:"search_switch"` //搜索开关
|
||||||
|
SearchEngine string `json:"search_engine"` //搜索引擎
|
||||||
|
ShortcutsSwitch bool `json:"shortcuts_switch"`
|
||||||
|
WidgetsSwitch bool `json:"widgets_switch"`
|
||||||
|
BackgroundType string `json:"background_type"`
|
||||||
|
Background string `json:"background"`
|
||||||
|
AutoUpdate bool `json:"auto_update"`
|
||||||
|
}
|
3
model/system.go
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
|
11
model/version.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Version struct {
|
||||||
|
Id uint `gorm:"column:id;primary_key" json:"id"`
|
||||||
|
ChangLog string `json:"chang_log"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
9
model/zerotier.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type ZeroTierUpData struct {
|
||||||
|
Config ZeroTierConfig `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZeroTierConfig struct {
|
||||||
|
Private bool `json:"private"`
|
||||||
|
}
|
6
model/zima.go
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type Path struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
5
pkg/config/config.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
const (
|
||||||
|
USERCONFIGURL = "conf/conf.ini"
|
||||||
|
)
|
84
pkg/config/init.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-ini/ini"
|
||||||
|
"log"
|
||||||
|
"oasis/model"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
//系统配置
|
||||||
|
var SysInfo = &model.SysInfoModel{}
|
||||||
|
|
||||||
|
//用户相关
|
||||||
|
var UserInfo = &model.UserModel{}
|
||||||
|
|
||||||
|
//用户相关
|
||||||
|
var AppInfo = &model.APPModel{}
|
||||||
|
|
||||||
|
//redis相关配置
|
||||||
|
var RedisInfo = &model.RedisModel{}
|
||||||
|
|
||||||
|
//zerotier相关
|
||||||
|
var ZeroTierInfo = &model.ZeroTierModel{}
|
||||||
|
|
||||||
|
//server相关
|
||||||
|
var ServerInfo = &model.ServerModel{}
|
||||||
|
|
||||||
|
var SystemConfigInfo = &model.SystemConfig{}
|
||||||
|
|
||||||
|
var Cfg *ini.File
|
||||||
|
|
||||||
|
//初始化设置,获取系统的部分信息。
|
||||||
|
func InitSetup(config string) {
|
||||||
|
|
||||||
|
var configDir = USERCONFIGURL
|
||||||
|
if len(config) > 0 {
|
||||||
|
configDir = config
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
//读取文件
|
||||||
|
Cfg, err = ini.Load(configDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Fail to read file: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
mapTo("user", UserInfo)
|
||||||
|
mapTo("app", AppInfo)
|
||||||
|
mapTo("zerotier", ZeroTierInfo)
|
||||||
|
mapTo("redis", RedisInfo)
|
||||||
|
mapTo("server", ServerInfo)
|
||||||
|
mapTo("system", SystemConfigInfo)
|
||||||
|
AppInfo.ProjectPath = getCurrentDirectory() //os.Getwd()
|
||||||
|
}
|
||||||
|
|
||||||
|
//映射
|
||||||
|
func mapTo(section string, v interface{}) {
|
||||||
|
err := Cfg.Section(section).MapTo(v)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Cfg.MapTo %s err: %v", section, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前执行文件绝对路径(go run)
|
||||||
|
func getCurrentAbPathByCaller() string {
|
||||||
|
var abPath string
|
||||||
|
_, filename, _, ok := runtime.Caller(0)
|
||||||
|
if ok {
|
||||||
|
abPath = path.Dir(filename)
|
||||||
|
}
|
||||||
|
return abPath
|
||||||
|
}
|
||||||
|
func getCurrentDirectory() string {
|
||||||
|
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return strings.Replace(dir, "\\", "/", -1)
|
||||||
|
}
|
15
pkg/ddns/emum.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package ddns
|
||||||
|
|
||||||
|
const (
|
||||||
|
GOGADDY = iota
|
||||||
|
GOOGLE
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
A = "A"
|
||||||
|
AAAA = "AAAA"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GODADDYAPIURL = "https://api.godaddy.com"
|
||||||
|
)
|
3
pkg/docker/emum.go
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
const NETWORKNAME = "oasis"
|
248
pkg/docker/helper.go
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
json2 "encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSshClient() (*ssh.Client, error) {
|
||||||
|
config := &ssh.ClientConfig{
|
||||||
|
Timeout: time.Second * 5,
|
||||||
|
User: "root",
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
//HostKeyCallback: ,
|
||||||
|
//HostKeyCallback: hostKeyCallBackFunc(h.Host),
|
||||||
|
}
|
||||||
|
//if h.Type == "password" {
|
||||||
|
config.Auth = []ssh.AuthMethod{ssh.Password("123456")}
|
||||||
|
//} else {
|
||||||
|
// config.Auth = []ssh.AuthMethod{publicKeyAuthFunc(h.Key)}
|
||||||
|
//}
|
||||||
|
addr := fmt.Sprintf("%s:%d", "192.168.2.142", 22)
|
||||||
|
c, err := ssh.Dial("tcp", addr, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup ssh shell session
|
||||||
|
// set Session and StdinPipe here,
|
||||||
|
// and the Session.Stdout and Session.Sdterr are also set.
|
||||||
|
func NewSshConn(cols, rows int, sshClient *ssh.Client) (*SshConn, error) {
|
||||||
|
sshSession, err := sshClient.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stdinP, err := sshSession.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
comboWriter := new(wsBufferWriter)
|
||||||
|
|
||||||
|
sshSession.Stdout = comboWriter
|
||||||
|
sshSession.Stderr = comboWriter
|
||||||
|
|
||||||
|
modes := ssh.TerminalModes{
|
||||||
|
ssh.ECHO: 1, // disable echo
|
||||||
|
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
|
||||||
|
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
|
||||||
|
}
|
||||||
|
// Request pseudo terminal
|
||||||
|
if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Start remote shell
|
||||||
|
if err := sshSession.Shell(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &SshConn{StdinPipe: stdinP, ComboOutput: comboWriter, Session: sshSession}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SshConn struct {
|
||||||
|
// calling Write() to write data into ssh server
|
||||||
|
StdinPipe io.WriteCloser
|
||||||
|
// Write() be called to receive data from ssh server
|
||||||
|
ComboOutput *wsBufferWriter
|
||||||
|
Session *ssh.Session
|
||||||
|
}
|
||||||
|
type wsBufferWriter struct {
|
||||||
|
buffer bytes.Buffer
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wsBufferWriter) Write(p []byte) (int, error) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
return w.buffer.Write(p)
|
||||||
|
}
|
||||||
|
func (s *SshConn) Close() {
|
||||||
|
if s.Session != nil {
|
||||||
|
s.Session.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
wsMsgCmd = "cmd"
|
||||||
|
wsMsgResize = "resize"
|
||||||
|
)
|
||||||
|
|
||||||
|
//ReceiveWsMsg receive websocket msg do some handling then write into ssh.session.stdin
|
||||||
|
func (ssConn *SshConn) ReceiveWsMsg(wsConn *websocket.Conn, logBuff *bytes.Buffer, exitCh chan bool) {
|
||||||
|
//tells other go routine quit
|
||||||
|
defer setQuit(exitCh)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-exitCh:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
//read websocket msg
|
||||||
|
_, wsData, err := wsConn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Error("reading webSocket message failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//unmashal bytes into struct
|
||||||
|
//msgObj := wsMsg{
|
||||||
|
// Type: "cmd",
|
||||||
|
// Cmd: "",
|
||||||
|
// Rows: 50,
|
||||||
|
// Cols: 180,
|
||||||
|
//}
|
||||||
|
msgObj := wsMsg{}
|
||||||
|
if err := json2.Unmarshal(wsData, &msgObj); err != nil {
|
||||||
|
msgObj.Type = "cmd"
|
||||||
|
msgObj.Cmd = string(wsData)
|
||||||
|
}
|
||||||
|
//if err := json.Unmarshal(wsData, &msgObj); err != nil {
|
||||||
|
// logrus.WithError(err).WithField("wsData", string(wsData)).Error("unmarshal websocket message failed")
|
||||||
|
//}
|
||||||
|
switch msgObj.Type {
|
||||||
|
|
||||||
|
case wsMsgResize:
|
||||||
|
//handle xterm.js size change
|
||||||
|
if msgObj.Cols > 0 && msgObj.Rows > 0 {
|
||||||
|
if err := ssConn.Session.WindowChange(msgObj.Rows, msgObj.Cols); err != nil {
|
||||||
|
logrus.WithError(err).Error("ssh pty change windows size failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case wsMsgCmd:
|
||||||
|
//handle xterm.js stdin
|
||||||
|
//decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Cmd)
|
||||||
|
decodeBytes := []byte(msgObj.Cmd)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Error("websock cmd string base64 decoding failed")
|
||||||
|
}
|
||||||
|
if _, err := ssConn.StdinPipe.Write(decodeBytes); err != nil {
|
||||||
|
logrus.WithError(err).Error("ws cmd bytes write to ssh.stdin pipe failed")
|
||||||
|
}
|
||||||
|
//write input cmd to log buffer
|
||||||
|
if _, err := logBuff.Write(decodeBytes); err != nil {
|
||||||
|
logrus.WithError(err).Error("write received cmd into log buffer failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ssConn *SshConn) SendComboOutput(wsConn *websocket.Conn, exitCh chan bool) {
|
||||||
|
//tells other go routine quit
|
||||||
|
//defer setQuit(exitCh)
|
||||||
|
|
||||||
|
//every 120ms write combine output bytes into websocket response
|
||||||
|
tick := time.NewTicker(time.Millisecond * time.Duration(120))
|
||||||
|
//for range time.Tick(120 * time.Millisecond){}
|
||||||
|
defer tick.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-tick.C:
|
||||||
|
//write combine output bytes into websocket response
|
||||||
|
if err := flushComboOutput(ssConn.ComboOutput, wsConn); err != nil {
|
||||||
|
logrus.WithError(err).Error("ssh sending combo output to webSocket failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-exitCh:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func flushComboOutput(w *wsBufferWriter, wsConn *websocket.Conn) error {
|
||||||
|
if w.buffer.Len() != 0 {
|
||||||
|
err := wsConn.WriteMessage(websocket.TextMessage, w.buffer.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.buffer.Reset()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (ssConn *SshConn) SessionWait(quitChan chan bool) {
|
||||||
|
if err := ssConn.Session.Wait(); err != nil {
|
||||||
|
logrus.WithError(err).Error("ssh session wait failed")
|
||||||
|
setQuit(quitChan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setQuit(ch chan bool) {
|
||||||
|
ch <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
type wsMsg struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Cmd string `json:"cmd"`
|
||||||
|
Cols int `json:"cols"`
|
||||||
|
Rows int `json:"rows"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将终端的输出转发到前端
|
||||||
|
func WsWriterCopy(reader io.Reader, writer *websocket.Conn) {
|
||||||
|
buf := make([]byte, 8192)
|
||||||
|
reg1 := regexp.MustCompile(`stty rows \d+ && stty cols \d+ `)
|
||||||
|
for {
|
||||||
|
nr, err := reader.Read(buf)
|
||||||
|
if nr > 0 {
|
||||||
|
result1 := reg1.FindIndex(buf[0:nr])
|
||||||
|
if len(result1) > 0 {
|
||||||
|
fmt.Println(result1)
|
||||||
|
} else {
|
||||||
|
err := writer.WriteMessage(websocket.BinaryMessage, buf[0:nr])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将前端的输入转发到终端
|
||||||
|
func WsReaderCopy(reader *websocket.Conn, writer io.Writer) {
|
||||||
|
for {
|
||||||
|
messageType, p, err := reader.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if messageType == websocket.TextMessage {
|
||||||
|
msgObj := wsMsg{}
|
||||||
|
if err = json2.Unmarshal(p, &msgObj); err != nil {
|
||||||
|
writer.Write(p)
|
||||||
|
} else if msgObj.Type == wsMsgResize {
|
||||||
|
writer.Write([]byte("stty rows " + strconv.Itoa(msgObj.Rows) + " && stty cols " + strconv.Itoa(msgObj.Cols) + " \r" ))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
pkg/docker/volumes.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
func GetDir(id, envName string) string {
|
||||||
|
var path string
|
||||||
|
switch envName {
|
||||||
|
case "/config":
|
||||||
|
path = "/oasis/app_data/" + id + "/"
|
||||||
|
default:
|
||||||
|
//path = "/media"
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
25
pkg/github/github.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package github
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/google/go-github/v36/github"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func GetGithubClient() *github.Client {
|
||||||
|
ctx := context.Background()
|
||||||
|
ts := oauth2.StaticTokenSource(
|
||||||
|
&oauth2.Token{AccessToken: "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"},
|
||||||
|
)
|
||||||
|
tc := oauth2.NewClient(ctx, ts)
|
||||||
|
client := github.NewClient(tc)
|
||||||
|
return client
|
||||||
|
|
||||||
|
// list all repositories for the authenticated user
|
||||||
|
//repos, _, err := client.Repositories.List(ctx, "", nil)
|
||||||
|
|
||||||
|
//fmt.Print(err)
|
||||||
|
//fmt.Print(repos)
|
||||||
|
|
||||||
|
}
|
7
pkg/github/github_test.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package github
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestGetRepos(t *testing.T) {
|
||||||
|
GetRepos()
|
||||||
|
}
|
33
pkg/gredis/redis.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package gredis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gomodule/redigo/redis"
|
||||||
|
"oasis/model"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetRedisConn(m *model.RedisModel) *redis.Pool {
|
||||||
|
redisConn := &redis.Pool{
|
||||||
|
MaxIdle: m.MaxIdle,
|
||||||
|
MaxActive: m.MaxActive,
|
||||||
|
IdleTimeout: m.IdleTimeout * time.Second,
|
||||||
|
Dial: func() (redis.Conn, error) {
|
||||||
|
c, err := redis.Dial("tcp", m.Host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if m.Password != "" {
|
||||||
|
if _, err := c.Do("AUTH", m.Password); err != nil {
|
||||||
|
c.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
},
|
||||||
|
TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||||
|
_, err := c.Do("PING")
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return redisConn
|
||||||
|
}
|
38
pkg/sqlite/db.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"oasis/pkg/utils/file"
|
||||||
|
model2 "oasis/service/model"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var gdb *gorm.DB
|
||||||
|
|
||||||
|
func GetDb(projectPath string) *gorm.DB {
|
||||||
|
if gdb != nil {
|
||||||
|
return gdb
|
||||||
|
}
|
||||||
|
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
|
||||||
|
//dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8mb4&parseTime=True&loc=Local", m.User, m.PWD, m.IP, m.Port, m.DBName)
|
||||||
|
//db, err := gorm.Open(mysql2.Open(dsn), &gorm.Config{})
|
||||||
|
file.IsNotExistMkDir(projectPath + "/db/")
|
||||||
|
db, err := gorm.Open(sqlite.Open(projectPath+"/db/casaOS.db"), &gorm.Config{})
|
||||||
|
c, _ := db.DB()
|
||||||
|
c.SetMaxIdleConns(10)
|
||||||
|
c.SetMaxOpenConns(100)
|
||||||
|
c.SetConnMaxIdleTime(time.Second * 1000)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("连接数据失败!")
|
||||||
|
panic("数据库连接失败")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
gdb = db
|
||||||
|
err = db.AutoMigrate(&model2.TaskDBModel{}, &model2.AppNotify{}, &model2.AppListDBModel{})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("检查和创建数据库出错", err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
15
pkg/sqlite/db_test.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetDb(t *testing.T) {
|
||||||
|
// fmt.Println(GetDb())
|
||||||
|
// db:=GetDb()
|
||||||
|
// d:=model.DDNSTypeDBModel{
|
||||||
|
// Name: "test",
|
||||||
|
// ApiHost: "http://www.google.com",
|
||||||
|
// }
|
||||||
|
// db.Create(&d)
|
||||||
|
}
|
89
pkg/upnp/device.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package upnp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetCtrlUrl(host,device string) string {
|
||||||
|
request := ctrlUrlRequest(host, device)
|
||||||
|
response, _ := http.DefaultClient.Do(request)
|
||||||
|
resultBody, _ := ioutil.ReadAll(response.Body)
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode == 200 {
|
||||||
|
return resolve(string(resultBody))
|
||||||
|
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctrlUrlRequest(host string, deviceDescUrl string) *http.Request {
|
||||||
|
//请求头
|
||||||
|
header := http.Header{}
|
||||||
|
header.Set("Accept", "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2")
|
||||||
|
header.Set("User-Agent", "preston")
|
||||||
|
header.Set("Host", host)
|
||||||
|
header.Set("Connection", "keep-alive")
|
||||||
|
request, _ := http.NewRequest("GET", "http://"+host+deviceDescUrl, nil)
|
||||||
|
request.Header = header
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolve(resultStr string) string {
|
||||||
|
inputReader := strings.NewReader(resultStr)
|
||||||
|
|
||||||
|
// 从文件读取,如可以如下:
|
||||||
|
// content, err := ioutil.ReadFile("studygolang.xml")
|
||||||
|
// decoder := xml.NewDecoder(bytes.NewBuffer(content))
|
||||||
|
|
||||||
|
lastLabel := ""
|
||||||
|
|
||||||
|
ISUpnpServer := false
|
||||||
|
|
||||||
|
IScontrolURL := false
|
||||||
|
var controlURL string //`controlURL`
|
||||||
|
// var eventSubURL string //`eventSubURL`
|
||||||
|
// var SCPDURL string //`SCPDURL`
|
||||||
|
|
||||||
|
decoder := xml.NewDecoder(inputReader)
|
||||||
|
for t, err := decoder.Token(); err == nil && !IScontrolURL; t, err = decoder.Token() {
|
||||||
|
switch token := t.(type) {
|
||||||
|
// 处理元素开始(标签)
|
||||||
|
case xml.StartElement:
|
||||||
|
if ISUpnpServer {
|
||||||
|
name := token.Name.Local
|
||||||
|
lastLabel = name
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理元素结束(标签)
|
||||||
|
case xml.EndElement:
|
||||||
|
// log.Println("结束标记:", token.Name.Local)
|
||||||
|
// 处理字符数据(这里就是元素的文本)
|
||||||
|
case xml.CharData:
|
||||||
|
//得到url后其他标记就不处理了
|
||||||
|
content := string([]byte(token))
|
||||||
|
//找到提供端口映射的服务
|
||||||
|
if content == "urn:schemas-upnp-org:service:WANIPConnection:1" {
|
||||||
|
ISUpnpServer = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ISUpnpServer {
|
||||||
|
switch lastLabel {
|
||||||
|
case "controlURL":
|
||||||
|
controlURL = content
|
||||||
|
IScontrolURL = true
|
||||||
|
case "eventSubURL":
|
||||||
|
// eventSubURL = content
|
||||||
|
case "SCPDURL":
|
||||||
|
// SCPDURL = content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return controlURL
|
||||||
|
}
|
16
pkg/upnp/device_test.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package upnp
|
||||||
|
|
||||||
|
import (
|
||||||
|
ip_helper2 "oasis/pkg/utils/ip_helper"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetCtrlUrl(t *testing.T) {
|
||||||
|
upnp, err := Gateway()
|
||||||
|
if err == nil {
|
||||||
|
upnp.CtrlUrl = GetCtrlUrl(upnp.GatewayHost, upnp.DeviceDescUrl)
|
||||||
|
upnp.LocalHost = ip_helper2.GetLoclIp()
|
||||||
|
upnp.AddPortMapping(8090, 8090, "TCP")
|
||||||
|
//upnp.DelPortMapping(9999, "TCP")
|
||||||
|
}
|
||||||
|
}
|
76
pkg/upnp/gateway.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package upnp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"net"
|
||||||
|
ip_helper2 "oasis/pkg/utils/ip_helper"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Gateway() (*Upnp, error) {
|
||||||
|
result, error := send()
|
||||||
|
if result == "" || error != nil {
|
||||||
|
return nil, error
|
||||||
|
}
|
||||||
|
upnp := resolvesss(result)
|
||||||
|
return upnp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func send() (string, error) {
|
||||||
|
var str = "M-SEARCH * HTTP/1.1\r\n" +
|
||||||
|
"HOST: 239.255.255.250:1900\r\n" +
|
||||||
|
"ST: urn:schemas-upnp-org:service:WANIPConnection:1\r\n" +
|
||||||
|
"MAN: \"ssdp:discover\"\r\n" + "MX: 3\r\n\r\n"
|
||||||
|
var conn *net.UDPConn
|
||||||
|
remotAddr, err := net.ResolveUDPAddr("udp", "239.255.255.250:1900")
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("组播地址格式不正确")
|
||||||
|
}
|
||||||
|
locaAddr, err := net.ResolveUDPAddr("udp", ip_helper2.GetLoclIp()+":")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("本地ip地址格式不正确")
|
||||||
|
}
|
||||||
|
conn, err = net.ListenUDP("udp", locaAddr)
|
||||||
|
defer conn.Close()
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("监听udp出错")
|
||||||
|
}
|
||||||
|
_, err = conn.WriteToUDP([]byte(str), remotAddr)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("发送msg到组播地址出错")
|
||||||
|
}
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, _, err := conn.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("从组播地址接搜消息出错")
|
||||||
|
}
|
||||||
|
result := string(buf[:n])
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolvesss(result string) *Upnp {
|
||||||
|
var upnp = &Upnp{}
|
||||||
|
lines := strings.Split(result, "\r\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
//按照第一个冒号分为两个字符串
|
||||||
|
nameValues := strings.SplitAfterN(line, ":", 2)
|
||||||
|
if len(nameValues) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch strings.ToUpper(strings.Trim(strings.Split(nameValues[0], ":")[0], " ")) {
|
||||||
|
case "ST":
|
||||||
|
//fmt.Println(nameValues[1])
|
||||||
|
case "CACHE-CONTROL":
|
||||||
|
//fmt.Println(nameValues[1])
|
||||||
|
case "LOCATION":
|
||||||
|
urls := strings.Split(strings.Split(nameValues[1], "//")[1], "/")
|
||||||
|
upnp.GatewayHost = (urls[0])
|
||||||
|
upnp.DeviceDescUrl = ("/" + urls[1])
|
||||||
|
case "SERVER":
|
||||||
|
upnp.GatewayName = (nameValues[1])
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return upnp
|
||||||
|
}
|
8
pkg/upnp/gateway_test.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package upnp
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestGateway(t *testing.T) {
|
||||||
|
|
||||||
|
Gateway()
|
||||||
|
}
|
163
pkg/upnp/mapping.go
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
package upnp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
//
|
||||||
|
////添加一个端口映射
|
||||||
|
func (n *Upnp)AddPortMapping(localPort, remotePort int, protocol string) (err error) {
|
||||||
|
defer func(err error) {
|
||||||
|
if errTemp := recover(); errTemp != nil {
|
||||||
|
//log.Println("upnp模块报错了", errTemp)
|
||||||
|
err = errTemp.(error)
|
||||||
|
}
|
||||||
|
}(err)
|
||||||
|
if issuccess := addSend(localPort, remotePort, protocol,n.GatewayHost, n.CtrlUrl,n.LocalHost); issuccess {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return errors.New("添加一个端口映射失败")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func addSend(localPort, remotePort int, protocol, host, ctrUrl,localHost string) bool {
|
||||||
|
request := addRequest(localPort, remotePort, protocol, host, ctrUrl,localHost)
|
||||||
|
response, _ := http.DefaultClient.Do(request)
|
||||||
|
defer response.Body.Close()
|
||||||
|
//resultBody, _ := ioutil.ReadAll(response.Body)
|
||||||
|
//fmt.Println(string(resultBody))
|
||||||
|
if response.StatusCode == 200 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
Name string
|
||||||
|
Content string
|
||||||
|
Attr map[string]string
|
||||||
|
Child []Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func addRequest(localPort, remotePort int, protocol string, gatewayHost, ctlUrl,localHost string) *http.Request {
|
||||||
|
//请求头
|
||||||
|
header := http.Header{}
|
||||||
|
header.Set("Accept", "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2")
|
||||||
|
header.Set("SOAPAction", `"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping"`)
|
||||||
|
header.Set("Content-Type", "text/xml")
|
||||||
|
header.Set("Connection", "Close")
|
||||||
|
header.Set("Content-Length", "")
|
||||||
|
//请求体
|
||||||
|
body := Node{Name: "SOAP-ENV:Envelope",
|
||||||
|
Attr: map[string]string{"xmlns:SOAP-ENV": `"http://schemas.xmlsoap.org/soap/envelope/"`,
|
||||||
|
"SOAP-ENV:encodingStyle": `"http://schemas.xmlsoap.org/soap/encoding/"`}}
|
||||||
|
childOne := Node{Name: `SOAP-ENV:Body`}
|
||||||
|
childTwo := Node{Name: `m:AddPortMapping`,
|
||||||
|
Attr: map[string]string{"xmlns:m": `"urn:schemas-upnp-org:service:WANIPConnection:1"`}}
|
||||||
|
|
||||||
|
childList1 := Node{Name: "NewExternalPort", Content: strconv.Itoa(remotePort)}
|
||||||
|
childList2 := Node{Name: "NewInternalPort", Content: strconv.Itoa(localPort)}
|
||||||
|
childList3 := Node{Name: "NewProtocol", Content: protocol}
|
||||||
|
childList4 := Node{Name: "NewEnabled", Content: "1"}
|
||||||
|
childList5 := Node{Name: "NewInternalClient", Content: localHost}
|
||||||
|
childList6 := Node{Name: "NewLeaseDuration", Content: "0"}
|
||||||
|
childList7 := Node{Name: "NewPortMappingDescription", Content: "Oasis"}
|
||||||
|
childList8 := Node{Name: "NewRemoteHost"}
|
||||||
|
childTwo.AddChild(childList1)
|
||||||
|
childTwo.AddChild(childList2)
|
||||||
|
childTwo.AddChild(childList3)
|
||||||
|
childTwo.AddChild(childList4)
|
||||||
|
childTwo.AddChild(childList5)
|
||||||
|
childTwo.AddChild(childList6)
|
||||||
|
childTwo.AddChild(childList7)
|
||||||
|
childTwo.AddChild(childList8)
|
||||||
|
|
||||||
|
childOne.AddChild(childTwo)
|
||||||
|
body.AddChild(childOne)
|
||||||
|
bodyStr := body.BuildXML()
|
||||||
|
//请求
|
||||||
|
request, _ := http.NewRequest("POST", "http://"+gatewayHost+ctlUrl,
|
||||||
|
strings.NewReader(bodyStr))
|
||||||
|
request.Header = header
|
||||||
|
request.Header.Set("Content-Length", strconv.Itoa(len([]byte(bodyStr))))
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) AddChild(node Node) {
|
||||||
|
n.Child = append(n.Child, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Node) BuildXML() string {
|
||||||
|
buf := bytes.NewBufferString("<")
|
||||||
|
buf.WriteString(n.Name)
|
||||||
|
for key, value := range n.Attr {
|
||||||
|
buf.WriteString(" ")
|
||||||
|
buf.WriteString(key + "=" + value)
|
||||||
|
}
|
||||||
|
buf.WriteString(">" + n.Content)
|
||||||
|
|
||||||
|
for _, node := range n.Child {
|
||||||
|
buf.WriteString(node.BuildXML())
|
||||||
|
}
|
||||||
|
buf.WriteString("</" + n.Name + ">")
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Upnp)DelPortMapping(remotePort int, protocol string) bool {
|
||||||
|
issuccess := delSendSend(remotePort, protocol,n.GatewayHost,n.CtrlUrl)
|
||||||
|
if issuccess {
|
||||||
|
//this.MappingPort.delMapping(remotePort, protocol)
|
||||||
|
//fmt.Println("删除了一个端口映射: remote:", remotePort)
|
||||||
|
}
|
||||||
|
return issuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
func delSendSend(remotePort int, protocol,host,ctlUrl string) bool {
|
||||||
|
delrequest := delbuildRequest(remotePort, protocol,host,ctlUrl)
|
||||||
|
response, _ := http.DefaultClient.Do(delrequest)
|
||||||
|
//resultBody, _ := ioutil.ReadAll(response.Body)
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode == 200 {
|
||||||
|
// log.Println(string(resultBody))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
func delbuildRequest(remotePort int, protocol,host,ctlUrl string) *http.Request {
|
||||||
|
//请求头
|
||||||
|
header := http.Header{}
|
||||||
|
header.Set("Accept", "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2")
|
||||||
|
header.Set("SOAPAction", `"urn:schemas-upnp-org:service:WANIPConnection:1#DeletePortMapping"`)
|
||||||
|
header.Set("Content-Type", "text/xml")
|
||||||
|
header.Set("Connection", "Close")
|
||||||
|
header.Set("Content-Length", "")
|
||||||
|
//请求体
|
||||||
|
body := Node{Name: "SOAP-ENV:Envelope",
|
||||||
|
Attr: map[string]string{"xmlns:SOAP-ENV": `"http://schemas.xmlsoap.org/soap/envelope/"`,
|
||||||
|
"SOAP-ENV:encodingStyle": `"http://schemas.xmlsoap.org/soap/encoding/"`}}
|
||||||
|
childOne := Node{Name: `SOAP-ENV:Body`}
|
||||||
|
childTwo := Node{Name: `m:DeletePortMapping`,
|
||||||
|
Attr: map[string]string{"xmlns:m": `"urn:schemas-upnp-org:service:WANIPConnection:1"`}}
|
||||||
|
childList1 := Node{Name: "NewExternalPort", Content: strconv.Itoa(remotePort)}
|
||||||
|
childList2 := Node{Name: "NewProtocol", Content: protocol}
|
||||||
|
childList3 := Node{Name: "NewRemoteHost"}
|
||||||
|
childTwo.AddChild(childList1)
|
||||||
|
childTwo.AddChild(childList2)
|
||||||
|
childTwo.AddChild(childList3)
|
||||||
|
childOne.AddChild(childTwo)
|
||||||
|
body.AddChild(childOne)
|
||||||
|
bodyStr := body.BuildXML()
|
||||||
|
|
||||||
|
//请求
|
||||||
|
request, _ := http.NewRequest("POST", "http://"+host+ctlUrl,
|
||||||
|
strings.NewReader(bodyStr))
|
||||||
|
request.Header = header
|
||||||
|
request.Header.Set("Content-Length", strconv.Itoa(len([]byte(bodyStr))))
|
||||||
|
return request
|
||||||
|
}
|