A vue3 + ts refactor

This commit is contained in:
zhenorzz 2021-05-14 20:35:22 +08:00
parent b3f9088758
commit 9f22442f54
126 changed files with 13205 additions and 0 deletions

13
web+/.env.development Normal file
View File

@ -0,0 +1,13 @@
# base api
VITE_APP_BASE_API = '/api'
# devServer.proxy.target
VITE_APP_PROXY_TARGET = 'http://localhost:8098'
# vue-cli uses the VITE_CLI_BABEL_TRANSPILE_MODULES environment variable,
# to control whether the babel-plugin-dynamic-import-node plugin is enabled.
# It only does one thing by converting all import() to require().
# This configuration can significantly increase the speed of hot updates,
# when you have a large number of pages.
# Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js
VITE_CLI_BABEL_TRANSPILE_MODULES = true

3
web+/.env.production Normal file
View File

@ -0,0 +1,3 @@
# base api
VITE_APP_BASE_API = ''

15
web+/.eslintrc.json Normal file
View File

@ -0,0 +1,15 @@
{
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": 2020,
"sourceType": "module"
},
"extends": [
"plugin:vue/vue3-recommended",
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"rules": {}
}

7
web+/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.idea
.vscode

6
web+/.prettierrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"endOfLine": "auto",
"@typescript-eslint/no-explicit-any": ["off"]
}

27
web+/README.md Normal file
View File

@ -0,0 +1,27 @@
# Vue 3 + Typescript + Vite
This template should help get you started developing with Vue 3 and Typescript in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur). Make sure to enable `vetur.experimental.templateInterpolationService` in settings!
### If Using `<script setup>`
[`<script setup>`](https://github.com/vuejs/rfcs/pull/227) is a feature that is currently in RFC stage. To get proper IDE support for the syntax, use [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) instead of Vetur (and disable Vetur).
## Type Support For `.vue` Imports in TS
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can use the following:
### If Using Volar
Run `Volar: Switch TS Plugin on/off` from VSCode command palette.
### If Using Vetur
1. Install and add `@vuedx/typescript-plugin-vue` to the [plugins section](https://www.typescriptlang.org/tsconfig#plugins) in `tsconfig.json`
2. Delete `src/shims-vue.d.ts` as it is no longer needed to provide module info to Typescript
3. Open `src/main.ts` in VSCode
4. Open the VSCode command palette
5. Search and run "Select TypeScript version" -> "Use workspace version"

13
web+/index.html Normal file
View File

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

50
web+/package.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "my-vue-app",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"serve": "vite preview"
},
"dependencies": {
"@chenfengyuan/vue-qrcode": "^2.0.0-beta",
"axios": "^0.21.1",
"cronstrue": "^1.113.0",
"element-plus": "^1.0.2-beta.41",
"js-cookie": "^2.2.1",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
"path-to-regexp": "^6.2.0",
"qrcode": "^1.4.4",
"vue": "^3.0.5",
"vue-i18n": "^9.1.6",
"vue-router": "^4.0.6",
"vue3-ace-editor": "^2.0.2",
"vuex": "^4.0.0",
"xterm": "^4.12.0",
"xterm-addon-attach": "^0.6.0",
"xterm-addon-fit": "^0.5.0"
},
"devDependencies": {
"@intlify/vite-plugin-vue-i18n": "^2.1.2",
"@types/codemirror": "^5.60.0",
"@types/js-cookie": "^2.2.6",
"@types/node": "^15.0.2",
"@types/nprogress": "^0.2.0",
"@typescript-eslint/eslint-plugin": "^4.22.1",
"@typescript-eslint/parser": "^4.22.1",
"@vitejs/plugin-vue": "^1.2.2",
"@vue/compiler-sfc": "^3.0.5",
"eslint": "^7.25.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^7.9.0",
"prettier": "^2.2.1",
"sass": "^1.32.12",
"typescript": "^4.2.4",
"vite": "^2.2.3",
"vite-plugin-svg-icons": "^0.5.0",
"vue-tsc": "^0.0.24"
}
}

BIN
web+/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

10
web+/src/App.vue Normal file
View File

@ -0,0 +1,10 @@
<template>
<router-view />
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'App',
})
</script>

76
web+/src/api/axios.ts Normal file
View File

@ -0,0 +1,76 @@
import axios, { AxiosResponse, AxiosRequestConfig, AxiosError } from 'axios'
import { ElMessageBox, ElMessage } from 'element-plus'
import store from '@/store'
// create an axios instance
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API, // url = base url + request url
withCredentials: true, // send cookies when cross-domain requests
timeout: 5000, // request timeout
})
// request interceptor
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
// do something before request is sent
return config
},
(error: AxiosError) => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
(response: AxiosResponse) => {
const res = response.data
if (res.code !== 0) {
ElMessage({
message: res.message,
type: 'error',
duration: 5 * 1000,
})
// 10000:账户停用; 10001:非法Token; 10086:Token 过期了;
if ([10000, 10001, 10086].includes(res.code)) {
ElMessageBox.confirm(res.message, '确定登出', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
store.dispatch('user/logout').then(() => {
location.reload()
})
})
return Promise.reject('error')
} else {
return Promise.reject('error')
}
} else {
return res
}
},
(error: AxiosError) => {
console.log('err' + error) // for debug
ElMessage({
message: error.message,
type: 'error',
duration: 5 * 1000,
})
return Promise.reject(error)
}
)
export default service

95
web+/src/api/crontab.ts Normal file
View File

@ -0,0 +1,95 @@
import Axios from './axios'
/**
* @return {Promise}
*/
export function getList({ page, rows }, command) {
return Axios.request({
url: '/crontab/getList',
method: 'get',
params: { page, rows, command },
})
}
/**
* @return {Promise}
*/
export function getTotal(command) {
return Axios.request({
url: '/crontab/getTotal',
method: 'get',
params: { command },
})
}
/**
* @return {Promise}
*/
export function getRemoteServerList(serverId) {
return Axios.request({
url: '/crontab/getRemoteServerList',
method: 'get',
params: { serverId },
timeout: 0,
})
}
/**
* @param {id} id
* @return {Promise}
*/
export function getBindServerList(id) {
return Axios.request({
url: '/crontab/getBindServerList',
method: 'get',
params: { id },
})
}
export function add(data) {
return Axios.request({
url: '/crontab/add',
method: 'post',
data,
})
}
export function edit(data) {
return Axios.request({
url: '/crontab/edit',
method: 'put',
data,
})
}
export function importCrontab(data) {
return Axios.request({
url: '/crontab/import',
method: 'post',
data,
})
}
export function remove(data) {
return Axios.request({
url: '/crontab/remove',
method: 'delete',
data,
})
}
export function addServer(data) {
return Axios.request({
url: '/crontab/addServer',
method: 'post',
data,
})
}
export function removeCrontabServer(data) {
return Axios.request({
url: '/crontab/removeCrontabServer',
method: 'delete',
data,
})
}

161
web+/src/api/deploy.ts Normal file
View File

@ -0,0 +1,161 @@
import Axios from './axios'
/**
* @param {string} projectName
* @return {Promise}
*/
export function getList(projectName) {
return Axios.request({
url: '/deploy/getList',
method: 'get',
params: { projectName },
})
}
/**
* @param {string} lastPublishToken
* @return {Promise}
*/
export function getPublishTrace(lastPublishToken) {
return Axios.request({
url: '/deploy/getPublishTrace',
method: 'get',
params: {
lastPublishToken,
},
})
}
/**
* @param {Number} publish_trace_id
* @return {Promise}
*/
export function getPublishTraceDetail(publish_trace_id) {
return Axios.request({
url: '/deploy/getPublishTraceDetail',
method: 'get',
params: {
publish_trace_id,
},
timeout: 0,
})
}
/**
* @param {object} pagination
* @param {object} params
* @return {Promise}
*/
export function getPreview({ page, rows }, params) {
return Axios.request({
url: '/deploy/getPreview',
method: 'get',
params: {
page,
rows,
...params,
},
})
}
/**
* @param {int} id
* @return {Promise}
*/
export function getCommitList(id, branch) {
return Axios.request({
url: '/deploy/getCommitList',
method: 'get',
params: { id, branch },
timeout: 0,
})
}
/**
* @param {int} id
* @return {Promise}
*/
export function getBranchList(id) {
return Axios.request({
url: '/deploy/getBranchList',
method: 'get',
params: { id },
timeout: 0,
})
}
/**
* @param {int} id
* @return {Promise}
*/
export function getTagList(id) {
return Axios.request({
url: '/deploy/getTagList',
method: 'get',
params: { id },
timeout: 0,
})
}
/**
* @param {int} projectId
* @return {Promise}
*/
export function resetState(projectId) {
return Axios.request({
url: '/deploy/resetState',
method: 'put',
data: { projectId },
})
}
/**
* @param {int} projectId
* @param {string} commit
* @return {Promise}
*/
export function publish(projectId, branch, commit) {
return Axios.request({
url: '/deploy/publish',
method: 'post',
data: { projectId, branch, commit },
})
}
/**
* @param {string} token
* @return {Promise}
*/
export function rebuild(projectId, token) {
return Axios.request({
url: '/deploy/rebuild',
method: 'post',
data: { projectId, token },
})
}
/**
* @param {int} projectId
* @param {string} commit
* @param {Array} serverIds
* @return {Promise}
*/
export function greyPublish(projectId, commit, serverIds) {
return Axios.request({
url: '/deploy/greyPublish',
method: 'post',
data: { projectId, commit, serverIds },
})
}
/**
* @param {int} projectReviewId
* @param {int} state
* @return {Promise}
*/
export function review(projectReviewId, state) {
return Axios.request({
url: '/deploy/review',
method: 'put',
data: { projectReviewId, state },
})
}

64
web+/src/api/monitor.ts Normal file
View File

@ -0,0 +1,64 @@
import Axios from './axios'
/**
* @return {Promise}
*/
export function getList({ page, rows }) {
return Axios.request({
url: '/monitor/getList',
method: 'get',
params: { page, rows },
})
}
/**
* @return {Promise}
*/
export function getTotal() {
return Axios.request({
url: '/monitor/getTotal',
method: 'get',
params: {},
})
}
export function add(data) {
return Axios.request({
url: '/monitor/add',
method: 'post',
data,
})
}
export function edit(data) {
return Axios.request({
url: '/monitor/edit',
method: 'put',
data,
})
}
export function check(data) {
return Axios.request({
timeout: 100000,
url: '/monitor/check',
method: 'post',
data,
})
}
export function toggle(id) {
return Axios.request({
url: '/monitor/toggle',
method: 'put',
data: { id },
})
}
export function remove(id) {
return Axios.request({
url: '/monitor/remove',
method: 'delete',
data: { id },
})
}

79
web+/src/api/namespace.ts Normal file
View File

@ -0,0 +1,79 @@
import Axios from './axios'
/**
* @return {Promise}
*/
export function getList({ page, rows }) {
return Axios.request({
url: '/namespace/getList',
method: 'get',
params: { page, rows },
})
}
/**
* @return {Promise}
*/
export function getTotal() {
return Axios.request({
url: '/namespace/getTotal',
method: 'get',
params: {},
})
}
/**
* @return {Promise}
*/
export function getUserOption() {
return Axios.request({
url: '/namespace/getUserOption',
method: 'get',
})
}
/**
* @param {id} id
* @return {Promise}
*/
export function getBindUserList(id) {
return Axios.request({
url: '/namespace/getBindUserList',
method: 'get',
params: { id },
})
}
export function add(data) {
return Axios.request({
url: '/namespace/add',
method: 'post',
data,
})
}
export function edit(data) {
return Axios.request({
url: '/namespace/edit',
method: 'put',
data,
})
}
export function addUser(data) {
return Axios.request({
url: '/namespace/addUser',
method: 'post',
data,
})
}
export function removeUser(namespaceUserId) {
return Axios.request({
url: '/namespace/removeUser',
method: 'delete',
data: {
namespaceUserId,
},
})
}

231
web+/src/api/project.ts Normal file
View File

@ -0,0 +1,231 @@
import Axios from './axios'
/**
* @return {Promise}
*/
export function getList({ page, rows }, projectName) {
return Axios.request({
url: '/project/getList',
method: 'get',
params: { page, rows, projectName },
})
}
/**
* @return {Promise}
*/
export function getTotal(projectName) {
return Axios.request({
url: '/project/getTotal',
method: 'get',
params: { projectName },
})
}
/**
* @return {Promise}
*/
export function getRemoteBranchList(url) {
return Axios.request({
url: '/project/getRemoteBranchList',
method: 'get',
timeout: 0,
params: { url },
})
}
/**
* @param {id} id
* @return {Promise}
*/
export function getBindServerList(id) {
return Axios.request({
url: '/project/getBindServerList',
method: 'get',
params: { id },
})
}
/**
* @param {id} id
* @return {Promise}
*/
export function getBindUserList(id) {
return Axios.request({
url: '/project/getBindUserList',
method: 'get',
params: { id },
})
}
/**
* @param {id} id
* @return {Promise}
*/
export function getProjectFileList(id) {
return Axios.request({
url: '/project/getProjectFileList',
method: 'get',
params: { id },
})
}
/**
* @param {id} id
* @return {Promise}
*/
export function getProjectFileContent(id) {
return Axios.request({
url: '/project/getProjectFileContent',
method: 'get',
params: { id },
})
}
/**
* @param {string} project
* @param {string} owner
* @param {string} repository
* @param {string} serverIds
* @param {string} userIds
* @return {Promise}
*/
export function add(data) {
return Axios.request({
url: '/project/add',
method: 'post',
data,
})
}
/**
* @param {string} project
* @param {string} owner
* @param {string} repository
* @return {Promise}
*/
export function edit(data) {
return Axios.request({
url: '/project/edit',
method: 'put',
data,
})
}
export function setAutoDeploy(data) {
return Axios.request({
url: '/project/setAutoDeploy',
method: 'put',
data,
})
}
export function remove(id) {
return Axios.request({
url: '/project/remove',
method: 'delete',
data: { id },
})
}
export function addUser(data) {
return Axios.request({
url: '/project/addUser',
method: 'post',
data,
})
}
export function removeUser(projectUserId) {
return Axios.request({
url: '/project/removeUser',
method: 'delete',
data: {
projectUserId,
},
})
}
export function addServer(data) {
return Axios.request({
url: '/project/addServer',
method: 'post',
data,
})
}
export function removeServer(projectServerId) {
return Axios.request({
url: '/project/removeServer',
method: 'delete',
data: {
projectServerId,
},
})
}
export function addFile(data) {
return Axios.request({
url: '/project/addFile',
method: 'post',
data,
})
}
export function editFile(data) {
return Axios.request({
url: '/project/editFile',
method: 'put',
data,
})
}
export function removeFile(projectFileId) {
return Axios.request({
url: '/project/removeFile',
method: 'delete',
data: {
projectFileId,
},
})
}
export function addTask(data) {
return Axios.request({
url: '/project/addTask',
method: 'post',
data,
})
}
export function removeTask(id) {
return Axios.request({
url: '/project/removeTask',
method: 'delete',
data: { id },
})
}
/**
* @param {id} id
* @return {Promise}
*/
export function getTaskList({ page, rows }, id) {
return Axios.request({
url: '/project/getTaskList',
method: 'get',
params: { page, rows, id },
})
}
/**
* @param {id} id
* @return {Promise}
*/
export function getReviewList({ page, rows }, id) {
return Axios.request({
url: '/project/getReviewList',
method: 'get',
params: { page, rows, id },
})
}

77
web+/src/api/server.ts Normal file
View File

@ -0,0 +1,77 @@
import Axios from './axios'
/**
* @return {Promise}
*/
export function getList({ page, rows }) {
return Axios.request({
url: '/server/getList',
method: 'get',
params: { page, rows },
})
}
/**
* @return {Promise}
*/
export function getTotal() {
return Axios.request({
url: '/server/getTotal',
method: 'get',
params: {},
})
}
/**
* @return {Promise}
*/
export function getPublicKey(path) {
return Axios.request({
url: '/server/getPublicKey',
method: 'get',
params: { path },
})
}
/**
* @return {Promise}
*/
export function getOption() {
return Axios.request({
url: '/server/getOption',
method: 'get',
})
}
export function add(data) {
return Axios.request({
url: '/server/add',
method: 'post',
data,
})
}
export function edit(data) {
return Axios.request({
url: '/server/edit',
method: 'put',
data,
})
}
export function check(data) {
return Axios.request({
timeout: 100000,
url: '/server/check',
method: 'post',
data,
})
}
export function remove(id) {
return Axios.request({
url: '/server/remove',
method: 'delete',
data: { id },
})
}

19
web+/src/api/types.ts Normal file
View File

@ -0,0 +1,19 @@
// 接口响应通过格式
export type HttpResponse<T> = {
code: number
message: string
data: T
}
export type Pagination = {
page: number
rows: number
}
export type Total = {
total: number
}
export type ID = {
id: number
}

91
web+/src/api/user.ts Normal file
View File

@ -0,0 +1,91 @@
import Axios from './axios'
import { HttpResponse, Pagination, Total, ID } from './types'
type LoginParams = {
account: string
password: string
}
type LoginResp = {
namespaceList: Array<{ id: number; name: string }>
}
export function login(data: LoginParams): Promise<HttpResponse<LoginResp>> {
return Axios.request({
url: '/user/login',
method: 'post',
data,
})
}
export function getInfo(): Promise<HttpResponse> {
return Axios.request({
url: '/user/info',
method: 'get',
})
}
type listFilterParams = Record<string, unknown>
export function getList({
page,
rows,
}: Pagination | listFilterParams): Promise<HttpResponse> {
return Axios.request({
url: '/user/getList',
method: 'get',
params: { page, rows },
})
}
export function getTotal(): Promise<HttpResponse<Total>> {
return Axios.request({
url: '/user/getTotal',
method: 'get',
params: {},
})
}
export function getOption(): Promise<HttpResponse> {
return Axios.request({
url: '/user/getOption',
method: 'get',
})
}
export function add(data): Promise<HttpResponse<ID>> {
return Axios.request({
url: '/user/add',
method: 'post',
data,
})
}
export function edit(data): Promise<HttpResponse<never>> {
return Axios.request({
url: '/user/edit',
method: 'put',
data,
})
}
export function remove(id: number): Promise<HttpResponse<never>> {
return Axios.request({
url: '/user/remove',
method: 'delete',
data: { id },
})
}
export function changePassword(
oldPwd: string,
newPwd: string
): Promise<HttpResponse<never>> {
return Axios.request({
url: '/user/changePassword',
method: 'put',
data: {
oldPwd,
newPwd,
},
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,94 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
<span
v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
class="no-redirect"
>{{ $t(`route.${item.meta.title}`) }}</span
>
<a v-else @click.prevent="handleLink(item)">
{{ $t(`route.${item.meta.title}`) }}
</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script>
import { pathToRegexp } from 'path-to-regexp'
import { defineComponent } from 'vue'
export default defineComponent({
data() {
return {
levelList: null,
}
},
watch: {
$route() {
this.getBreadcrumb()
},
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
// only show routes with meta.title
const matched = this.$route.matched.filter(
(item) => item.meta && item.meta.title
)
this.levelList = matched.filter(
(item) => item.meta && item.meta.title && item.meta.breadcrumb !== false
)
},
pathCompile(path) {
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
const { params } = this.$route
var toPath = pathToRegexp.compile(path)
return toPath(params)
},
handleLink(item) {
const { redirect, path } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(this.pathCompile(path))
},
},
})
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
/* breadcrumb transition */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all 0.5s;
}
.breadcrumb-enter,
.breadcrumb-leave-active {
opacity: 1;
transform: translateX(20px);
}
.breadcrumb-move {
transition: all 0.5s;
}
.breadcrumb-leave-active {
position: absolute;
}
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<div style="padding: 0 15px" @click="toggleClick">
<svg
:class="{ 'is-active': isActive }"
class="hamburger"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
>
<path
d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
/>
</svg>
</div>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Hamburger',
props: {
isActive: {
type: Boolean,
default: false,
},
},
emits: ['toggleClick'],
methods: {
toggleClick() {
this.$emit('toggleClick')
},
},
})
</script>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<div
v-if="isExternal"
:style="styleExternalIcon"
class="svg-external-icon svg-icon"
v-bind="$attrs"
/>
<svg v-else :class="svgClass" aria-hidden="true" v-bind="$attrs">
<use :xlink:href="iconName" />
</svg>
</template>
<script>
import { isExternal } from '@/utils/validate'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true,
},
className: {
type: String,
default: '',
},
},
computed: {
isExternal() {
return isExternal(this.iconClass)
},
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon() {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`,
}
},
},
})
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover !important;
display: inline-block;
}
</style>

7
web+/src/global.ts Normal file
View File

@ -0,0 +1,7 @@
export default {
// 媒体类型
Admin: 'admin',
Manager: 'manager',
GroupManager: 'group-manager',
Member: 'member',
}

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1545633395570" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2796" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M896.174 64.191l-832.162 0.078v831.693l384.075-0.322v-63.919l-320.062 0.367-0.035-703.775h704.171v127.64h64.012V64.191z" p-id="2797"></path><path d="M192.037 320.194h384.075v-63.919H192.037v63.919z m0 127.768l320.062 0.069v-63.919H192.037v63.85z m0 128.317h192.037v-63.891l-192.037-0.028v63.919z m0 0M768.105 368.128l-32.034-50.338-31.979 50.338v399.674h64.013z" p-id="2798"></path><path d="M704.137 448.209l31.934-130.419-159.959 194.337v86.748zM768.149 448.209L896.174 598.75v-86.623L736.071 317.789zM576.112 831.721h320.062v63.919H576.112z" p-id="2799"></path></svg>

After

Width:  |  Height:  |  Size: 955 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575771744006" class="icon" viewBox="0 0 1097 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6965" xmlns:xlink="http://www.w3.org/1999/xlink" width="214.2578125" height="200"><defs><style type="text/css"></style></defs><path d="M0 512v512h292.571429V512H0z m219.428571 438.857143H73.142857v-365.714286h146.285714v365.714286zM804.571429 438.857143v585.142857H1097.142857V438.857143h-292.571428z m219.428571 512h-146.285714V512h146.285714v438.857143zM365.714286 219.428571v804.571429h365.714285V219.428571H365.714286z m280.380952 725.333334H451.047619v-134.095238h188.952381v134.095238z m0-256H451.047619V554.666667h188.952381v134.095238z m0-249.904762H451.047619V298.666667h188.952381v140.190476z" p-id="6966"></path><path d="M256 365.714286H182.857143V73.142857h731.428571v219.428572h-73.142857l109.714286 109.714285 109.714286-109.714285h-73.142858V0H109.714286v365.714286H36.571429l109.714285 109.714285L256 365.714286z" p-id="6967"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1592706144854" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2125" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M512.000128 282.496c-171.4176 0-310.8608 138.5984-310.8608 308.9664 0 170.368 139.4432 308.9664 310.8608 308.9664 171.4176 0 310.8608-138.5984 310.8608-308.992 0-170.3424-139.4432-308.9408-310.8608-308.9408z m54.1696 339.328l-0.1792-0.3584a62.0544 62.0544 0 0 1-116.1728-30.0032c0-21.5552 11.008-40.448 27.7248-51.3792l-0.1792-0.3584 204.9792-116.5312-116.1728 198.656zM849.484928 318.464l39.3216-39.0912a17.5616 17.5616 0 0 0 0-24.96l-37.6576-37.4528a17.8176 17.8176 0 0 0-25.1392 0L786.688128 256a434.3552 434.3552 0 0 0-230.272-94.8736V88.2688h53.2736c19.6352 0 35.5328-15.7952 35.5328-35.3024v-17.664c0-19.5072-15.872-35.3024-35.5328-35.3024h-195.3792c-19.6352 0-35.5328 15.7952-35.5328 35.328v17.6384c0 19.5072 15.872 35.328 35.5328 35.328h53.2736v72.832A434.6368 434.6368 0 0 0 237.312128 256L197.990528 216.9088a17.8176 17.8176 0 0 0-25.1392 0L135.193728 254.3616a17.5616 17.5616 0 0 0 0 24.96l39.3216 39.0912A429.1072 429.1072 0 0 0 76.800128 591.4368C76.800128 830.3104 271.667328 1024 512.000128 1024s435.2-193.6896 435.2-432.5632c0-103.5264-36.6336-198.528-97.7152-272.9984zM512.000128 935.7312c-190.976 0-346.368-154.4704-346.368-344.2688C165.632128 401.664 321.024128 247.168 512.000128 247.168c190.976 0 346.368 154.496 346.368 344.2944S702.976128 935.7312 512.000128 935.7312z" p-id="2126"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1 @@
<svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1598768807022" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16655" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M864.48 256H672.224a95.68 95.68 0 0 0-83.072-48.736c-2.752 0-5.376 0.576-8.064 0.832L480.512 32H187.36L41.056 288l86.464 151.264a95.168 95.168 0 0 0-18.336 55.968c0 27.264 11.552 51.776 29.888 69.28L41.056 736 187.36 992h293.152L581.92 814.528c2.432 0.192 4.768 0.736 7.264 0.736A95.68 95.68 0 0 0 671.424 768h193.088l146.272-256L864.48 256z m-512 448l-80.16-140.32c5.92-5.792 10.816-12.512 15.104-19.68H443.36l54.848 96 13.312 23.296c-8.576 11.84-14.368 25.664-16.8 40.704H352.48z m142.4-384c2.624 14.752 8.608 28.224 17.152 39.84L498.208 384l-54.848 96H299.616a95.2 95.2 0 0 0-15.904-39.648L352.48 320h142.4zM205.184 527.264c-17.632 0-32-14.368-32-32s14.368-32 32-32 32 14.368 32 32-14.368 32-32 32zM498.784 512l66.368-116.16c7.712 2.016 15.68 3.424 24 3.424 8.8 0 17.184-1.568 25.312-3.808L681.056 512l-65.888 115.296a94.656 94.656 0 0 0-26.016-4.032c-8.608 0-16.8 1.504-24.736 3.648L498.784 512z m90.368-240.736a32.032 32.032 0 0 1 0 64 32 32 0 0 1 0-64zM114.784 288L224.48 96h218.848l79.104 138.464A96.256 96.256 0 0 0 506.112 256H315.36L231.2 403.296a94.464 94.464 0 0 0-26.016-4.032c-8.608 0-16.8 1.504-24.736 3.648L114.784 288z m328.544 640H224.48L114.784 736l83.136-145.472c2.432 0.192 4.768 0.736 7.264 0.736 2.976 0 5.76-0.608 8.672-0.864L315.36 768h191.584c4.48 7.552 9.792 14.496 16.128 20.512L443.328 928z m145.824-176.736a32 32 0 1 1 0.064-64 32 32 0 0 1-0.064 64zM827.328 704H683.584a95.04 95.04 0 0 0-15.904-39.648L754.752 512 667.2 358.752A95.186 95.186 0 0 0 683.456 320h143.872l109.728 192-109.728 192z" p-id="16656"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg>

After

Width:  |  Height:  |  Size: 497 B

View File

@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>

After

Width:  |  Height:  |  Size: 944 B

View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.068 23.784c-1.02 0-1.877-.32-2.572-.96a8.588 8.588 0 0 1-1.738-2.237 11.524 11.524 0 0 1-1.042-2.621c-.232-.895-.348-1.641-.348-2.238V0h.278c.834 0 1.622.085 2.363.256.742.17 1.645.575 2.711 1.214 1.066.64 2.363 1.535 3.892 2.686 1.53 1.15 3.453 2.664 5.77 4.54 2.502 2.045 4.494 3.771 5.977 5.178 1.483 1.406 2.618 2.6 3.406 3.58.787.98 1.274 1.812 1.46 2.494.185.682.277 1.278.277 1.79v2.046H84.068zM127.3 84.01c.278.682.464 1.535.556 2.558.093 1.023-.37 2.003-1.39 2.94-.463.427-.88.832-1.25 1.215-.372.384-.696.704-.974.96a6.69 6.69 0 0 1-.973.767l-11.816-10.741a44.331 44.331 0 0 0 1.877-1.535 31.028 31.028 0 0 1 1.737-1.406c1.112-.938 2.317-1.343 3.615-1.215 1.297.128 2.363.405 3.197.83.927.427 1.923 1.173 2.989 2.239 1.065 1.065 1.876 2.195 2.432 3.388zM78.23 95.902c2.038 0 3.752-.511 5.143-1.534l-26.969 25.83H18.037c-1.761 0-3.684-.47-5.77-1.407a24.549 24.549 0 0 1-5.838-3.709 21.373 21.373 0 0 1-4.518-5.306c-1.204-2.003-1.807-4.07-1.807-6.202V16.495c0-1.79.44-3.665 1.32-5.626A18.41 18.41 0 0 1 5.04 5.562a21.798 21.798 0 0 1 5.213-3.964C12.198.533 14.237 0 16.37 0h53.24v15.984c0 1.62.278 3.367.834 5.242a16.704 16.704 0 0 0 2.572 5.179c1.159 1.577 2.665 2.898 4.518 3.964 1.853 1.066 4.078 1.598 6.673 1.598h20.295v42.325L85.458 92.45c1.02-1.364 1.529-2.856 1.529-4.476 0-2.216-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1c-2.409 0-4.448.789-6.116 2.366-1.668 1.577-2.502 3.474-2.502 5.69 0 2.217.834 4.092 2.502 5.626 1.668 1.535 3.707 2.302 6.117 2.302h52.13zM26.1 47.951c-2.41 0-4.449.789-6.117 2.366-1.668 1.577-2.502 3.473-2.502 5.69 0 2.216.834 4.092 2.502 5.626 1.668 1.534 3.707 2.302 6.117 2.302h52.13c2.409 0 4.47-.768 6.185-2.302 1.715-1.534 2.572-3.41 2.572-5.626 0-2.217-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1zm52.407 64.063l1.807-1.663 3.476-3.196a479.75 479.75 0 0 0 4.587-4.284 500.757 500.757 0 0 1 5.004-4.667c3.985-3.666 8.48-7.758 13.485-12.276l11.677 10.741-13.485 12.404-5.004 4.603-4.587 4.22a179.46 179.46 0 0 0-3.267 3.068c-.88.853-1.367 1.322-1.46 1.407-.463.341-.973.703-1.529 1.087-.556.383-1.112.703-1.668.959-.556.256-1.413.575-2.572.959a83.5 83.5 0 0 1-3.545 1.087 72.2 72.2 0 0 1-3.475.895c-1.112.256-1.946.426-2.502.511-1.112.17-1.854.043-2.224-.383-.371-.426-.464-1.151-.278-2.174.092-.511.278-1.279.556-2.302.278-1.023.602-2.067.973-3.132l1.042-3.005c.325-.938.58-1.577.765-1.918a10.157 10.157 0 0 1 2.224-2.941z"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1592707158372" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13326" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M602.88 0v448.597333H1024C1024 448.597333 1025.450667 31.573333 602.88 0zM474.368 58.026667C212.48 58.026667 0 274.090667 0 541.013333 0 807.765333 212.48 1024 474.453333 1024c261.973333 0 489.386667-200.618667 489.386667-448H481.962667L474.368 58.026667z" p-id="13327"></path></svg>

After

Width:  |  Height:  |  Size: 661 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575608380967" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2669" xmlns:xlink="http://www.w3.org/1999/xlink" width="360" height="360"><defs><style type="text/css"></style></defs><path d="M720 912v-97.952l78.672-0.896V599.664a93.648 93.648 0 0 1 26.704-65.92A90.72 90.72 0 0 1 858.64 512a90.688 90.688 0 0 1-33.264-21.712 94.912 94.912 0 0 1-26.704-65.904V210.928L720 209.952V112l84.128 0.128a89.6 89.6 0 0 1 64.48 27.296 93.52 93.52 0 0 1 26.72 65.904v175.264a82.48 82.48 0 0 0 23.504 58.016 78.96 78.96 0 0 0 56.768 24H1024v98.784h-48.384a78.96 78.96 0 0 0-56.768 24 82.48 82.48 0 0 0-23.504 58.016V818.72a94.816 94.816 0 0 1-26.72 65.92A89.6 89.6 0 0 1 804.128 912z m-500.064 0c-58.448-14.896-91.2-48.816-91.2-93.2V643.488a82.448 82.448 0 0 0-23.504-58.016 78.944 78.944 0 0 0-56.768-24H0.096v-98.832h48.336a81.28 81.28 0 0 0 80.288-82.048V205.344a92.32 92.32 0 0 1 91.2-93.2L304 112v97.952l-78.608 0.96v213.488A93.264 93.264 0 0 1 165.296 512a93.296 93.296 0 0 1 60.096 87.632v213.504l78.608 0.912V912zM672 736.192A48.16 48.16 0 1 1 719.84 688 48 48 0 0 1 672 736.192z m-160 0A48.16 48.16 0 1 1 559.856 688 48 48 0 0 1 512 736.192z m-160 0A48.16 48.16 0 1 1 399.872 688 48 48 0 0 1 352 736.192z" p-id="2670"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.742 36.8c2.398 7.2 5.595 12.8 11.19 18.4 4.795-4.8 7.992-11.2 10.39-18.4h-21.58zm-52.748 40h20.78l-10.39-28-10.39 28z"/><path d="M111.916 0H16.009C7.218 0 .025 7.2.025 16v96c0 8.8 7.193 16 15.984 16h95.907c8.791 0 15.984-7.2 15.984-16V16c0-8.8-6.394-16-15.984-16zM72.754 103.2c-1.598 1.6-3.197 1.6-4.795 1.6-.8 0-2.398 0-3.197-.8-.8-.8-1.599 0-1.599-.8s-.799-1.6-1.598-3.2c-.8-1.6-.8-2.4-1.599-4l-3.196-8.8H28.797L25.6 96c-1.598 3.2-2.398 5.6-3.197 7.2-.8 1.6-2.398 1.6-4.795 1.6-1.599 0-3.197-.8-4.796-1.6-1.598-1.6-2.397-2.4-2.397-4 0-.8 0-1.6.799-3.2.8-1.6.8-2.4 1.598-4l17.583-44.8c.8-1.6.8-3.2 1.599-4.8.799-1.6 1.598-3.2 2.397-4 .8-.8 1.599-2.4 3.197-3.2 1.599-.8 3.197-.8 4.796-.8 1.598 0 3.196 0 4.795.8 1.598.8 2.398 1.6 3.197 3.2.799.8 1.598 2.4 2.397 4 .8 1.6 1.599 3.2 2.398 5.6l17.583 44c1.598 3.2 2.398 5.6 2.398 7.2-.8.8-1.599 2.4-2.398 4zM116.711 72c-8.791-3.2-15.185-7.2-20.78-12-5.594 5.6-12.787 9.6-21.579 12l-2.397-4c8.791-2.4 15.984-5.6 21.579-11.2C87.939 51.2 83.144 44 81.545 36h-7.992v-3.2h21.58c-1.6-2.4-3.198-5.6-4.796-8l2.397-.8c1.599 2.4 3.997 5.6 5.595 8.8h19.98v4h-7.992c-2.397 8-6.393 15.2-11.189 20 5.595 4.8 11.988 8.8 20.78 11.2l-3.197 4z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1545633841664" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10675" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M384 320l512 0c38.4 0 64-25.6 64-64 0-38.4-25.6-64-64-64L384 192C345.6 192 320 217.6 320 256 320 294.4 345.6 320 384 320zM896 448 384 448C345.6 448 320 473.6 320 512c0 38.4 25.6 64 64 64l512 0c38.4 0 64-25.6 64-64C960 473.6 934.4 448 896 448zM896 704 384 704c-38.4 0-64 25.6-64 64 0 38.4 25.6 64 64 64l512 0c38.4 0 64-25.6 64-64C960 729.6 934.4 704 896 704zM128 192C89.6 192 64 217.6 64 256c0 38.4 25.6 64 64 64s64-25.6 64-64C192 217.6 166.4 192 128 192zM128 448C89.6 448 64 473.6 64 512c0 38.4 25.6 64 64 64s64-25.6 64-64C192 473.6 166.4 448 128 448zM128 704c-38.4 0-64 25.6-64 64 0 38.4 25.6 64 64 64s64-25.6 64-64C192 729.6 166.4 704 128 704z" p-id="10676"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595921111595" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3556" xmlns:xlink="http://www.w3.org/1999/xlink" width="360" height="360"><defs><style type="text/css"></style></defs><path d="M96 32h832a64 64 0 0 1 64 64v608a64 64 0 0 1-64 64H96a64 64 0 0 1-64-64V96a64 64 0 0 1 64-64z m262.528 415.36l59.456-119.552 132.8 243.584c25.6 46.848 84.224 49.28 115.2 6.208L747.52 464H832a48 48 0 1 0 0-96H722.944a48 48 0 0 0-38.976 19.968l-70.4 97.984-140.352-257.28c-26.752-49.088-89.088-48.32-114.176 2.048l-73.216 147.2h-76.8a48 48 0 0 0 0 96H315.52a48 48 0 0 0 43.008-26.624zM224 864h576a64 64 0 0 1 0 128H224a64 64 0 0 1 0-128z" fill="" p-id="3557"></path></svg>

After

Width:  |  Height:  |  Size: 847 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1595416180039" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3543" xmlns:xlink="http://www.w3.org/1999/xlink" width="360" height="360"><defs><style type="text/css"></style></defs><path d="M58.578341 815.715292l358.311759 202.364515a32.42383 32.42383 0 0 0 48.891722-26.621671V597.84991a30.717312 30.717312 0 0 0-15.728402-26.507902l-358.311759-205.350921A32.42383 32.42383 0 0 0 42.70773 392.49899v396.623073c0.113768 11.092363 6.200346 21.246141 15.927495 26.593229H58.578341z m531.665479 203.587519l358.311759-205.350921a30.717312 30.717312 0 0 0 15.728402-26.536344V393.835762a32.42383 32.42383 0 0 0-48.891722-26.621671l-358.311759 202.364515a30.717312 30.717312 0 0 0-15.870611 26.593229v396.623073a32.42383 32.42383 0 0 0 49.033931 26.507903zM487.881222 4.351619L79.625388 211.522825a30.262241 30.262241 0 0 0 0 53.129573l408.255834 230.322958c10.295988 5.830601 22.867332 5.830601 33.16332 0l405.582289-230.322958a30.262241 30.262241 0 0 0 0-53.129573L521.0161 4.351619a33.73216 33.73216 0 0 0-33.16332 0z" p-id="3544"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg>

After

Width:  |  Height:  |  Size: 821 B

View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>

After

Width:  |  Height:  |  Size: 623 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1563850301574" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="21816" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M679.413406 45.033659c-156.304905 0-283.008632 126.702704-283.008632 283.008632 0 17.7165 1.641383 35.035958 4.754281 51.846832L56.796052 724.253148l0 169.805384c0 31.273261 25.328875 56.602136 56.602136 56.602136l56.602136 0 0-56.603159 113.202225 0L283.202549 780.855284l113.203248 0L396.405797 667.652036l84.146469 0c27.922956-117.059066 133.172045-204.086142 258.789021-204.086142 57.558927 0 110.839411 18.273179 154.369833 49.330522 42.815131-49.590442 68.710918-114.196879 68.710918-184.855148C962.422038 171.735339 835.720358 45.033659 679.413406 45.033659zM764.203534 328.153832c-46.895053 0-84.902692-38.007639-84.902692-84.902692 0-46.89403 38.007639-84.902692 84.902692-84.902692 46.89403 0 84.902692 38.008662 84.902692 84.902692C849.106226 290.147216 811.097564 328.153832 764.203534 328.153832z" p-id="21817"></path><path d="M952.278004 684.147732 910.091182 684.147732c-3.974521-8.568143-9.614985-26.39209-16.829294-39.093367l29.795607-28.954449c9.408277-9.408277 9.416464-24.208355 0.017396-33.598212l-34.009582-33.80492c-9.391904-9.393951-24.619724-9.29162-34.028001 0.11154l-28.133757 29.835516c-12.681834-7.205099-30.504758-12.801561-39.073924-16.777105l0-42.179658c0-13.298887-14.087856-24.060999-27.361161-24.060999l-48.099485 0c-13.281491 0-27.369348 10.762111-27.369348 24.060999l0 42.187845c-8.569166 3.975545-26.38288 9.614985-39.08211 16.822131l-28.964682-29.795607c-9.407254-9.4093-24.215518-9.4093-33.600259-0.01842l-33.80492 34.020838c-9.383718 9.381671-9.296737 24.609491 0.112564 34.028001l29.845749 28.130687c-7.216355 12.69923-12.802584 30.514991-16.779152 39.08518L520.549212 684.147732c-13.308097 0-24.070208 14.086833-24.070208 27.359115l0 48.097438c0 13.275351 10.763135 27.371394 24.070208 27.371394l42.185798 0c3.976568 8.570189 9.624195 26.383904 16.821107 39.083134l-29.79356 28.972868c-9.4093 9.407254-9.4093 24.197098-0.017396 33.589003l34.009582 33.815153c9.381671 9.379625 24.618701 9.28548 34.029024-0.121773l28.130687-29.845749c12.69923 7.209192 30.512944 12.803607 39.08211 16.778128l0 42.185798c0 13.298887 14.087856 24.064069 27.361161 24.064069l48.099485 0c13.272281 0 27.369348-10.765181 27.369348-24.071232l0-42.185798c8.569166-3.977591 26.381857-9.621125 39.090297-16.82827l28.965705 29.79356c9.407254 9.4093 24.198122 9.4093 33.590026 0.016373l33.81413-34.008558c9.383718-9.391904 9.286504-24.62177-0.12075-34.026978l-29.846772-28.132734c7.207146-12.701277 12.803607-30.505781 16.778128-39.07597l42.188868 0c13.297864 0 24.062022-14.08888 24.062022-27.361161l0-48.099485C976.348212 698.233542 965.586101 684.147732 952.278004 684.147732zM736.414631 843.763033c-59.769268 0-108.22077-48.449455-108.22077-108.210537 0-59.771315 48.451502-108.218724 108.22077-108.218724 59.778478 0 108.219747 48.447409 108.219747 108.218724C844.633355 795.313577 796.193109 843.763033 736.414631 843.763033z" p-id="21818"></path></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1560152932744" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1984" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M512.056 64C414.191 139.35 310.2 191.848 131 191.848l0 331.27C131 681.06 331.43 960 512.056 960 692.683 960 893 681.116 893 523.118l0-331.27C713 191.848 609.808 139.406 512.056 64zM769.056 406.93l-272.8 272.8c-0.931 0.934-2.191 1.421-3.448 1.421-1.256 0-2.473-0.487-3.449-1.421L313.257 503.625c-0.896-0.935-1.382-2.151-1.382-3.449 0-1.301 0.529-2.517 1.421-3.449l55.832-55.796c1.907-1.906 4.993-1.906 6.86 0l116.86 116.863L706.364 344.28c1.907-1.907 4.994-1.907 6.858 0l55.835 55.832C770.963 401.979 770.963 405.023 769.056 406.93z" p-id="1985"></path></svg>

After

Width:  |  Height:  |  Size: 944 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1545633168327" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2047" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M896 896l-45.44-45.12A63.808 63.808 0 0 1 896 832a64 64 0 0 0 64-64V128a64 64 0 0 0-64-64H256a64 64 0 0 0-64 64v5.44c0 17.6-7.04 33.536-18.56 45.12L128 133.44V128A128 128 0 0 1 256 0h640a128 128 0 0 1 128 128v640a128 128 0 0 1-128 128zM64 256v640a64 64 0 0 0 64 64h640a64 64 0 0 0 64-64V256a64 64 0 0 0-64-64H128a64 64 0 0 0-64 64z m704-128a128 128 0 0 1 128 128v640a128 128 0 0 1-128 128H128A128 128 0 0 1 0 896V256a128 128 0 0 1 128-128h640z" p-id="2048"></path><path d="M160 256h384a32 32 0 0 1 0 64H160a32 32 0 0 1 0-64z m576 64a32 32 0 1 1 0-64 32 32 0 0 1 0 64zM64 384h768v64H64v-64z" p-id="2049"></path></svg>

After

Width:  |  Height:  |  Size: 1002 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1545633899485" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12269" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M755.2 839.68a34.304 34.304 0 1 1 36.352-34.304 34.816 34.816 0 0 1-36.352 34.304m-116.736 0a34.304 34.304 0 1 1 36.352-34.304 34.816 34.816 0 0 1-36.352 34.304z m-416.256-68.096h204.8v68.096h-204.8z m-55.296-81.92h-61.44v190.976a41.472 41.472 0 0 0 44.032 40.96h729.6a42.496 42.496 0 0 0 44.032-40.96v-190.976z m589.824-436.736A37.376 37.376 0 0 1 716.8 220.16a36.864 36.864 0 0 1 34.816-35.328 37.376 37.376 0 0 1 37.888 32.768 35.84 35.84 0 0 1-32.768 35.328z m-116.736 0a39.936 39.936 0 0 1-33.792-16.384 32.256 32.256 0 0 1 0-34.304 37.376 37.376 0 0 1 68.608 14.848 33.792 33.792 0 0 1-34.816 35.84zM222.208 184.32h204.8v68.608h-204.8zM861.696 102.4H167.936a59.904 59.904 0 0 0-61.44 57.344v175.104h819.2V159.744A61.952 61.952 0 0 0 861.696 102.4zM222.208 484.864h204.8v68.096h-204.8z m414.72 0a36.864 36.864 0 0 1 33.792 20.48 31.744 31.744 0 0 1-7.168 36.864 41.472 41.472 0 0 1-39.424 8.192 35.84 35.84 0 0 1-23.552-31.232 36.864 36.864 0 0 1 36.352-34.304z m116.736 0a37.376 37.376 0 0 1 37.888 32.768 36.864 36.864 0 0 1-34.816 35.328 37.376 37.376 0 0 1-37.888-32.768 35.84 35.84 0 0 1 34.816-35.328zM105.472 634.88H921.6V402.944H102.4z" p-id="12270"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1545633465087" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3580" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M595.84 960H428.32A52.48 52.48 0 0 1 376 914.56l-13.2-92.16a340.16 340.16 0 0 1-46.08-26.64l-88.96 35.44a53.28 53.28 0 0 1-64.48-24L80 664a52.96 52.96 0 0 1 12.96-67.92l74.4-57.76c-0.8-9.52-1.2-18.16-1.2-26.4s0-16.96 1.2-26.4l-74.24-57.68A52.64 52.64 0 0 1 80 360l83.68-144a52.56 52.56 0 0 1 64.8-22.88l88 35.2a351.28 351.28 0 0 1 46-26.64l13.52-92.8A52.32 52.32 0 0 1 428.32 64h167.52A52.48 52.48 0 0 1 648 109.44l13.44 92.16a339.68 339.68 0 0 1 46.08 26.64l88.88-35.44a53.2 53.2 0 0 1 64.48 24L944 360a52.96 52.96 0 0 1-12.96 67.92l-74.4 57.76c0.72 8 1.2 17.04 1.2 26.48s-0.48 18.32-1.2 26.48l74.24 57.68a52.8 52.8 0 0 1 13.44 67.28L860.48 808a52.56 52.56 0 0 1-64.8 22.88l-88-35.2a351.04 351.04 0 0 1-46 26.64L648 915.12A52.32 52.32 0 0 1 595.84 960z m-11.2-53.92z m-145.2-0.48z m-1.36-9.6h148.08l17.04-118.4 16.96-6.88a288 288 0 0 0 64-36.8l14.32-10.72 112 44.56 74-127.2-94.8-73.6 2.24-18.08a253.76 253.76 0 0 0 0-73.6l-2.24-18.08 94.8-73.6L809.92 256l-112 44.64-14.4-11.04a272.64 272.64 0 0 0-63.44-36.56l-16.88-6.64L586.16 128H438.08l-17.12 118.4-16.96 6.88a288.56 288.56 0 0 0-64 36.8l-14.08 10.72L214.24 256l-74 127.2 94.8 73.6-2.24 18.08a293.04 293.04 0 0 0-2.72 36.8 293.04 293.04 0 0 0 2.72 36.8l2.24 18.08-94.8 73.6 74 127.84 112-44.64 14.4 11.04a272.8 272.8 0 0 0 63.44 36.56l16.96 6.88z m367.04-120v0.48z m-586.32-0.56zM135.36 392zM805.2 248z m-585.84-0.64v0.48zM584.72 118z m-145.12-0.56zM512 689.6A177.6 177.6 0 1 1 690.64 512 178.32 178.32 0 0 1 512 689.6z m0-291.2A113.6 113.6 0 1 0 626.64 512 114.24 114.24 0 0 0 512 398.4z" p-id="3581"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg>

After

Width:  |  Height:  |  Size: 597 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566198179714" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1986" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M183.24 688.67c10.91-1.65 22.19-2.06 32.67-5.18 30.37-9 38.33-32.33 15.52-53.75-31.07-29.2-65.62-54.65-98.69-81.71C114.84 533.36 97 518.54 79 504.08c-15.63-12.5-19.25-30.14-9.71-45 29.5-46.05 58.82-92.74 93.59-134.67 23.11-27.86 47.1-19.26 60.38 15.56 3.63 9.51 5.5 19.69 8.44 29.49 8.71 29.11 27.06 39.53 55 27.6 48-20.52 88.26-52 116-96.93 20-32.39 11.38-54.24-24.38-67.12-16.55-6.01-34.32-8.88-50.12-16.25-24.29-11.35-31-32.79-10.73-48.8 45.61-36.1 94.16-68.85 143.76-99.27 19.1-11.71 39.57 0 53.32 18.31 25.8 34.24 51.32 68.71 77.49 102.66 9.65 12.51 19.22 26.1 31.76 35.05 11.38 8.13 28.35 17.09 40 14.17 20.65-5.16 28.1-24.78 26.49-46.82-0.73-10-1.29-20.72 1.42-30.14 8.08-28.14 36.17-42.09 61.57-28.28 48.62 26.42 87.38 63.86 107.32 116.8 17.28 45.87 1.49 66.11-47.14 69.45-14 1-36.77 8.09-39 16.66-3.25 12.72 3.88 34.5 14.25 43.22C830.65 415 875.28 447 919.24 479.64c48.27 35.89 52.55 53.74 18.74 104.44-23.21 34.81-47.06 69.75-75 100.68-30.79 34.13-60.85 26.41-68.84-18.63-13.74-77.46-61.36-78.23-109-30.37-26.33 26.46-48.42 58.87-65.86 92.08-16.57 31.56-2.37 53.19 31.8 62.82 21.79 6.14 54 7.15 53.89 34.26-0.1 18.45-20.11 40.46-36.66 54.33-29.1 24.39-61.61 45-94.05 65-42 25.86-62 20.49-91.82-19.85C451 881.68 419.32 839.1 387.1 797a79 79 0 0 0-22.91-20.06c-22.41-12.82-40.65-2.33-42.72 23.46-4.1 51.07-38.45 72.53-85.41 50.84-48.55-22.42-81.23-62.07-102.14-110.3-14.72-33.94 7.08-56.43 49.32-52.27z" p-id="1987"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575771663121" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4059" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M681.386667 258.986667V162.773333a54.186667 54.186667 0 0 0-48-48H369.066667a53.333333 53.333333 0 0 0-48 48v96.213334H177.066667a79.36 79.36 0 0 0-72.106667 71.893333v120.106667H921.6v-120.106667a78.08 78.08 0 0 0-72.106667-71.893333z m-72.106667 0H393.173333V210.773333a21.333333 21.333333 0 0 1 23.893334-23.893333h168.106666a22.613333 22.613333 0 0 1 24.106667 23.893333v48.213334zM512 594.346667c47.36 0 49.066667-58.24 49.066667-95.36h-119.893334c0 29.866667 0 96.213333 48 96.213333 0 0 13.226667-0.853333 22.826667-0.853333z m97.066667-95.36s37.973333 144.213333-108.16 144.213333c-150.826667 0-107.946667-144.213333-107.946667-144.213333H104.96v336.213333a74.24 74.24 0 0 0 72.106667 72.106667h672.426666a73.6 73.6 0 0 0 72.106667-72.106667V498.986667z" p-id="4060"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>

After

Width:  |  Height:  |  Size: 440 B

212
web+/src/lang/en.json Normal file
View File

@ -0,0 +1,212 @@
{
"name": "Name",
"script": "Script",
"user": "User",
"userId": "User ID",
"userName": "Username",
"admin": "Admin",
"role": "Role",
"account": "Account",
"password": "Password",
"mobile": "Mobile",
"contact": "Contact",
"project": "Project",
"projectName": "Project name",
"projectURL": "Project URL",
"projectPath": "Project path",
"autoDeploy": "Auto deploy",
"member": "Member",
"server": "Server",
"serverId": "Server ID",
"serverName": "Server name",
"serverDescription": "Server description",
"template": "Template",
"package": "Package",
"crontab": "Crontab",
"command": "Command",
"directory": "Directory",
"file": "File",
"func": "Func",
"param": "Param",
"deploy": "Deploy",
"grey": "Grey",
"initial": "Initial",
"search": "Search",
"tips": "Tips",
"view": "View",
"detail": "Detail",
"review": "Review",
"reject": "Reject",
"manage": "Manage",
"interval": "Interval",
"desc": "Description",
"size": "Size",
"namespace": "Namespace",
"environment": "Environment",
"branch": "Branch",
"filename": "Filename",
"setting": "Setting",
"baseSetting": "Base setting",
"notice": "Notice",
"task": "Task",
"date": "Date",
"now": "Now",
"today": "Today",
"m1d": "M1D",
"p1d": "P1D",
"time": "Time",
"insertTime": "Insert time",
"updateTime": "Update time",
"commitDate": "Commit date",
"deployDate": "Deploy date",
"startDate": "Start date",
"endDate": "End date",
"creator": "Creator",
"editor": "Editor",
"op": "Operation",
"submit": "Submit",
"add": "Add",
"edit": "Edit",
"copy": "Copy",
"approve": "Approve",
"deny": "Deny",
"upload": "Upload",
"uploading": "Uploading",
"reUpload": "Reupload",
"import": "Import",
"read": "Read",
"run": "Run",
"delete": "Delete",
"remove": "Remove",
"install": "Install",
"confirm": "Confirm",
"save": "Save",
"cancel": "Cancel",
"success": "Success",
"open": "Open",
"close": "Close",
"stop": "Stop",
"fail": "Fail",
"state": "State",
"stateOption": ["Disable", "Enable"],
"boolOption": ["No", "Yes"],
"runOption": ["Not run", "Run"],
"envOption": ["Unknown", "Production", "Pre-release", "Test", "Development"],
"webhookOption": {
"0": "Nothing",
"1": "Weixin",
"2": "Dingding",
"3": "Feishu",
"255": "Custom"
},
"route": {
"userProfile": "Profile",
"deploy": "Deploy",
"toolbox": "Toolbox",
"json": "JSON Formatter",
"monitor": "APP Monitor",
"project": "Project",
"server": "Server",
"serverSetting": "Server",
"template": "Template",
"crontab": "Crontab",
"namespace": "Namespace",
"member": "Member"
},
"tagsView": {
"refresh": "Refresh",
"close": "Close",
"closeOthers": "Close Others",
"closeAll": "Close All"
},
"navbar": {
"profile": "Your profile",
"doc": "Document",
"logout": "Sign out"
},
"userPage": {
"oldPassword": "Old password",
"newPassword": "New password",
"rePassword": "Confirm password"
},
"memberPage": {
"permissionDesc": "Admin has all namespace and project permissions",
"removeUserTips": "This action will delete the user({name}), continue?"
},
"namespacePage": {
"removeUserTips": "This action will delete the user\"s binding relationship, continue?"
},
"templatePage": {
"scriptNotice": "Note\": The package has been uploaded to the /tmp directory",
"templateDeleteTips":
"This action will delete the template({templateName}), continue?"
},
"serverPage": {
"sshKeyOwner": "SSH-Key Owner",
"sshKeyPath": "SSH-Key Path",
"sshKeyPassword": "SSH-Key Pswd",
"copyPub": "Copy pub",
"copyPubTips": "Copy success. Paste it to ~/.ssh/authorized_keys",
"testConnection": "Test",
"removeServerTips":
"This action will delete the server({serverName}), continue?"
},
"crontabPage": {
"removeServerCrontabLabel": "Delete crontab in the server",
"importTips": "Please read the crontab in the server",
"selectServerTips": "Please select the server",
"selectItemTips": "Please select an item",
"removeCrontabServerTips": "This action will delete the server\"s binding relationship, continue?"
},
"monitorPage": {
"testAppState": "Test APP state",
"failTimes": "Failure times",
"notifyTimes": "Notice times",
"errorContent": "Error content",
"toggleStateTips":
"This action will suspend the monitoring application({monitorName}), continue?",
"removeMontiorTips":
"This action will no longer monitor the app({monitorName}), continue?"
},
"JSONPage": {
"expandAll": "Expand all",
"collapseAll": "Collapse all",
"unmarkAll": "Unmark all",
"tips":
"1.Hold down ALT and click label to achieve highlighting<br>2.Hold down SHIFT and click label to view the JSON path"
},
"projectPage": {
"testConnection": "Test",
"lishBranch": "List branch",
"scriptMode": "Script mode",
"rsyncDoc": "https://linux.die.net/man/1/rsync",
"deployNotice": "Deploy notice",
"symlinkLabel": "Symlink deploy",
"symlinkHeaderTips": "<p>The project synchronize to the specified directory(rsync /symlinkPath), and ln -s projectPath symlinkPath</p><p>It can prevent the project from external access that are being synchronized during the process of synchronizing files</p><p>Back up the latest 10 deployment files for quick rollback</p>",
"symlinkFooterTips": "<p>If the deployment path already exists on the target server, please delete the directory manually <span style=\"color: red\">(rm -rf projectPath)</span>, otherwise the soft chain will fail</p><p>If you need to change the directory, you must manually migrate the original directory/p>",
"afterPullScriptLabel": "After pull script",
"afterPullScriptTips": "The script that runs on the host server after pull<br>For example: bash after-pull-script.sh <br>",
"afterDeployScriptLabel": "After deploy script",
"afterDeployScriptTips": "The script that runs on the target server after deploy<br>For example: bash after-deploy-script.sh<br>If you need to restart the service, please pay attention to whether you need nohup<br>",
"predefinedVar": "predefined vars",
"autoDeployTitle": "Deploy trigger: automatically build the release project after certain conditions are met",
"autoDeployTips": "Go to the webhook page in GitLab、GitHub or Gitee <br>Fill in URL <span style=\"color: red\">http(s)://domian(IP)/deploy/webhook?project_id={projectId}</span><br>Check push event, (Gitlab can choose the corresponding branch)",
"projectFileTips": "Upload to the target server with the project deploy.",
"removeProjectTips": "This action will delete the project({projectName}), continue?",
"removeFileTips": "This action will delete the file({filename}), continue?",
"removeServerTips": "This action will delete the server\"s({serverName}) binding relationship, continue?",
"removeUserTips": "This action will delete the user\"s({userName}) binding relationship, continue?"
},
"deployPage": {
"resetState": "Reset state",
"showDetail": "Show detail",
"noDetail": "No detail",
"taskDeploy": "Crontab deploy",
"resetStateTips": "This action will reset project state, continue?",
"reviewDeploy": "Review deploy",
"reviewTips": "This action will approve commit, continue?",
"reviewStateOption": ["Wait", "Reviewed", "Rejected"],
"removeProjectTaskTips": "This action will delete the crontab task in {projectName}, continue?",
"publishCommitTips": "This action will rebuild {commit}, continue?"
}
}

41
web+/src/lang/index.ts Normal file
View File

@ -0,0 +1,41 @@
import { createI18n } from 'vue-i18n'
import Cookies from 'js-cookie'
import elementEnLocale from 'element-plus/lib/locale/lang/en' // element-ui lang
import elementZhLocale from 'element-plus/lib/locale/lang/zh-cn'
import localMessages from '@intlify/vite-plugin-vue-i18n/messages'
const messages = {
[elementEnLocale.name]: {
// el 这个属性很关键,一定要保证有这个属性,
el: elementEnLocale.el,
// 定义您自己的字典,但是请不要和 `el` 重复,这样会导致 ElementPlus 内部组件的翻译失效.
...localMessages[elementEnLocale.name],
},
[elementZhLocale.name]: {
el: elementZhLocale.el,
// 定义您自己的字典,但是请不要和 `el` 重复,这样会导致 ElementPlus 内部组件的翻译失效.
...localMessages[elementZhLocale.name],
},
}
export function getLanguage(): string {
const chooseLanguage = Cookies.get('language')
if (chooseLanguage) return chooseLanguage
// if has not choose language
const language = navigator.language.toLowerCase()
const locales = Object.keys(messages)
for (const locale of locales) {
if (language.indexOf(locale) > -1) {
return locale
}
}
return 'en'
}
const i18n = createI18n({
globalInjection: true,
locale: getLanguage(),
messages,
})
export default i18n

209
web+/src/lang/zh-cn.json Normal file
View File

@ -0,0 +1,209 @@
{
"name": "名称",
"script": "脚本",
"user": "用户",
"userId": "用户ID",
"userName": "用户名称",
"admin": "超管",
"role": "角色",
"account": "账号",
"password": "密码",
"mobile": "手机号码",
"contact": "联系方式",
"project": "项目",
"projectName": "项目名称",
"projectURL": "项目地址",
"projectPath": "部署路径",
"autoDeploy": "自动部署",
"member": "成员",
"server": "服务器",
"serverId": "服务器ID",
"serverName": "服务器名称",
"serverDescription": "服务器描述",
"template": "模板",
"package": "安装包",
"crontab": "定时",
"command": "命令",
"directory": "目录",
"file": "文件",
"func": "功能",
"param": "参数",
"deploy": "构建",
"grey": "灰度",
"initial": "初始化",
"search": "搜索",
"tips": "提示",
"view": "查看",
"detail": "详情",
"review": "审核",
"reject": "拒绝",
"manage": "管理",
"interval": "间隔",
"desc": "描述",
"size": "大小",
"namespace": "空间",
"environment": "环境",
"branch": "分支",
"filename": "文件名",
"setting": "设置",
"baseSetting": "基本配置",
"notice": "通知",
"task": "任务",
"date": "日期",
"now": "当前",
"today": "今天",
"m1d": "减一天",
"p1d": "加一天",
"time": "时间",
"insertTime": "创建时间",
"updateTime": "更新时间",
"commitDate": "提交日期",
"deployDate": "构建日期",
"startDate": "开始日期",
"endDate": "结束日期",
"creator": "创建人",
"editor": "修改人",
"op": "操作",
"submit": "提交",
"add": "添加",
"edit": "编辑",
"copy": "复制",
"approve": "同意",
"deny": "拒绝",
"upload": "上传",
"uploading": "上传中",
"reUpload": "重传",
"import": "导入",
"read": "读取",
"run": "运行",
"delete": "删除",
"remove": "移除",
"install": "安装",
"confirm": "确认",
"save": "保存",
"cancel": "取消",
"success": "成功",
"open": "开启",
"close": "关闭",
"stop": "暂停",
"fail": "失败",
"state": "状态",
"stateOption": ["失效", "生效"],
"boolOption": ["否", "是"],
"runOption": ["未运行", "已运行"],
"envOption": ["未知", "生产环境", "预发布环境", "测试环境", "开发环境"],
"webhookOption": {
"0": "无",
"1": "企业微信",
"2": "钉钉",
"3": "飞书",
"255": "自定义"
},
"route": {
"userProfile": "个人信息",
"deploy": "构建发布",
"toolbox": "工具箱",
"json": "JSON格式化",
"monitor": "应用监控",
"project": "项目设置",
"server": "服务器管理",
"serverSetting": "服务器设置",
"template": "模板设置",
"crontab": "Crontab管理",
"namespace": "空间设置",
"member": "成员设置"
},
"tagsView": {
"refresh": "刷新",
"close": "关闭",
"closeOthers": "关闭其它",
"closeAll": "关闭所有"
},
"navbar": {
"profile": "个人中心",
"doc": "帮助文档",
"logout": "退出"
},
"userPage": {
"oldPassword": "原密码",
"newPassword": "新密码",
"rePassword": "确认新密码"
},
"memberPage": {
"permissionDesc": "超管具有所有空间和项目权限",
"removeUserTips": "此操作将删除{name}, 是否继续?"
},
"namespacePage": {
"removeUserTips": "此操作将永久删除该用户的绑定关系, 是否继续?"
},
"templatePage": {
"scriptNotice": "注意:安装包上传至目标服务器的/tmp目录",
"templateDeleteTips": "此操作将删除模板({templateName}), 是否继续?"
},
"serverPage": {
"sshKeyOwner": "SSH-Key 所有者",
"sshKeyPath": "SSH-Key 路径",
"sshKeyPassword": "SSH-Key 密码",
"copyPub": "复制共钥",
"copyPubTips": "复制成功,请粘贴到目标服务器~/.ssh/authorized_keys里面",
"testConnection": "测试连接",
"removeServerTips": "此操作将删除服务器({serverName}), 是否继续?"
},
"crontabPage": {
"removeServerCrontabLabel": "删除服务器Crontab任务",
"importTips": "请读取服务器Crontab任务",
"selectServerTips": "请先选择服务器",
"selectItemTips": "请先选择需要导入的条目",
"removeCrontabServerTips": "此操作将永久删除该服务器的绑定关系, 是否继续?"
},
"monitorPage": {
"testAppState": "测试应用状态",
"failTimes": "连续失败次数",
"notifyTimes": "通知次数",
"errorContent": "错误内容",
"toggleStateTips": "此操作将暂停监控应用({monitorName}), 是否继续?",
"removeMontiorTips": "此操作将不再监控应用({monitorName}), 是否继续?"
},
"JSONPage": {
"expandAll": "展开所有",
"collapseAll": "收起所有",
"unmarkAll": "取消高亮",
"tips": "1.按住ALT点击label可以实现高亮<br>2.按住SHIFT可以查看JSON路径"
},
"projectPage": {
"testConnection": "测试连接",
"lishBranch": "列出分支",
"scriptMode": "脚本类型",
"rsyncDoc": "https://zhenorzz.gitee.io/goploy/#/dependency/rsync",
"deployNotice": "构建通知",
"publishReview": "发布审核",
"reviewFooterTips": "只有成员构建项目才会触发审核<br>审核方式:<br>1. 前往构建发布页面进行审核<br>2. 推送到URL:http(s)://domain?custom-param=1&callback=***<br>&nbsp;&nbsp;&nbsp;&nbsp;http get callback的值即可完成审核<br>&nbsp;&nbsp;&nbsp;&nbsp;重复访问callback只会发布一次并且发布过不会再次发布",
"symlinkLabel": "软链部署(推荐)",
"symlinkHeaderTips": "<p>项目先同步到指定目录(rsync 软链目录)然后ln -s 部署路径 软链目录</p><p>可以避免项目在同步传输文件的过程中,外部访问到部分正在同步的文件</p><p>备份最近10次的部署文件以便快速回滚</p>",
"symlinkFooterTips": "<p>如果部署路径已存在在目标服务器,请手动删除该目录<span style=\"color: red\">rm -rf 部署路径</span>,否则软链将会不成功</p><p>如需更换目录,务必手动迁移原先的目录到目标服务器</p>",
"afterPullScriptLabel": "拉取后运行脚本",
"afterPullScriptTips": "拉取代码后在宿主服务器运行的脚本<br>运行方式:打包成一份脚本文件<br>检查服务器是否安装该脚本类型(默认以bash运行)<br>",
"afterDeployScriptLabel": "部署后运行脚本",
"afterDeployScriptTips": "部署后在目标服务器运行的脚本<br>运行方式:打包成一份脚本文件<br>如需重启服务请注意是否需要nohup<br>检查服务器是否安装该脚本类型(默认以bash运行)",
"predefinedVar": "预定义变量",
"autoDeployTitle": "构建触发器:达成某种条件后自动构建发布项目",
"autoDeployTips": "前往GitLab、GitHub或Gitee的webhook可前往谷歌查找各自webhook所在的位置<br>填入连接<span style=\"color: red\">http(s)://域名(IP)/deploy/webhook?project_id={projectId}</span><br>勾选push event即可, (Gitlab可以选对应的分支)",
"projectFileTips": "构建项目时上传到目标服务器",
"removeProjectTips": "此操作将删除项目({projectName}), 是否继续?",
"removeFileTips": "此操作将永久删除文件({filename}), 是否继续?",
"removeServerTips": "此操作将永久删除服务器({serverName})的绑定关系, 是否继续?",
"removeUserTips": "此操作将永久删除用户({userName})的绑定关系, 是否继续?"
},
"deployPage": {
"resetState": "重置状态",
"showDetail": "查看详情",
"noDetail": "暂无详情",
"taskDeploy": "定时构建",
"resetStateTips": "此操作将重置项目的构建状态, 是否继续?",
"reviewDeploy": "审核构建",
"reviewTips": "此操作将通过该次提交, 是否继续?",
"reviewStateOption": ["待审", "已审", "拒审"],
"removeProjectTaskTips": "此操作删除{projectName}的定时任务, 是否继续?",
"publishCommitTips": "此操作将重新构建{commit}, 是否继续?"
}
}

View File

@ -0,0 +1,51 @@
<template>
<section class="app-main">
<router-view v-slot="{ Component }" :key="key">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</section>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'AppMain',
computed: {
cachedViews() {
return import.meta.env.NODE_ENV === 'production'
? this.$store.state.tagsView.cachedViews
: []
// return this.$store.state.tagsView.cachedViews
},
key() {
return this.$route.path
},
},
})
</script>
<style scoped>
.app-main {
/*50 = navbar */
min-height: calc(100vh - 100px);
width: 100%;
position: relative;
overflow: hidden;
}
.fixed-header + .app-main {
padding-top: 50px;
}
</style>
<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
.fixed-header {
padding-right: 15px;
}
}
</style>

View File

@ -0,0 +1,403 @@
<template>
<div class="navbar">
<img :src="logo" class="navbar-logo" />
<hamburger
:is-active="app.sidebar.opened"
class="hamburger-container"
@toggleClick="toggleSideBar"
/>
<el-dropdown
style="float: left; line-height: 48px; cursor: pointer"
trigger="click"
size="medium"
placement="bottom-start"
@command="handleNamespaceChange"
>
<span class="el-dropdown-link">
{{ namespace.name }}<i class="el-icon-arrow-down el-icon--right" />
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in namespaceList"
:key="item.id"
:command="item"
>
{{ item.name }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<breadcrumb class="breadcrumb-container" />
<div class="right">
<div v-show="false" class="github">
<span class="github-btn">
<a class="gh-btn" href="https://github.com/zhenorzz" target="_blank">
<span class="gh-ico" />
<span class="gh-text">Follow @zhenorzz</span>
</a>
</span>
<span class="github-btn">
<a
class="gh-btn"
href="https://github.com/zhenorzz/goploy/"
target="_blank"
>
<span class="gh-ico" />
<span class="gh-text">Star</span>
</a>
<a
class="gh-count"
href="https://github.com/zhenorzz/goploy/stargazers"
target="_blank"
>
{{ starCount }}
</a>
</span>
<span class="github-btn github-forks">
<a
class="gh-btn"
href="https://github.com/zhenorzz/goploy/fork"
target="_blank"
>
<span class="gh-ico" aria-hidden="true" />
<span class="gh-text">Fork</span>
</a>
<a
class="gh-count"
href="https://github.com/zhenorzz/goploy/network"
target="_blank"
>
{{ forkCount }}
</a>
</span>
</div>
<div class="international">
<el-dropdown
trigger="click"
size="medium"
placement="bottom"
@command="handleSetLanguage"
>
<div>
<svg-icon class-name="international-icon" icon-class="language" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
:disabled="app.language === 'zh-cn'"
command="zh-cn"
>
中文
</el-dropdown-item>
<el-dropdown-item :disabled="app.language === 'en'" command="en">
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="user-menu">
<div class="user-container">
<el-dropdown trigger="click" size="medium">
<div class="user-wrapper">
<el-row type="flex">
<el-row>
<el-avatar
v-if="$store.getters.avatar"
:size="40"
:src="avatar"
/>
<div
v-else
class="avatar-box"
style="background: rgb(64, 158, 255)"
>
<span>{{ user.name.substr(0, 1) }}</span>
</div>
</el-row>
<div style="margin-left: 8px">
<div class="user-name">{{ user.name }}</div>
<div class="user-title">{{ namespace.role }}</div>
</div>
</el-row>
</div>
<template #dropdown>
<el-dropdown-menu class="user-dropdown">
<router-link to="/user/profile">
<el-dropdown-item>
{{ $t('navbar.profile') }}
</el-dropdown-item>
</router-link>
<el-link
:underline="false"
href="https://docs.goploy.icu/"
target="__blank"
>
<el-dropdown-item>
{{ $t('navbar.doc') }}
</el-dropdown-item>
</el-link>
<el-dropdown-item divided>
<span
style="display: block; text-align: center"
@click="logout"
>
{{ $t('navbar.logout') }}
</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
</div>
</template>
<script>
import logo from '@/assets/images/logo.png'
import { mapState } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb/index.vue'
import Hamburger from '@/components/Hamburger/index.vue'
import {
getNamespace,
getNamespaceList,
setNamespace,
removeNamespace,
} from '@/utils/namespace'
import { defineComponent } from 'vue'
export default defineComponent({
components: {
Breadcrumb,
Hamburger,
},
data() {
return {
logo: logo,
starCount: 0,
forkCount: 0,
namespace: getNamespace(),
namespaceList: getNamespaceList(),
}
},
computed: {
...mapState(['app', 'user']),
},
created() {
// fetch('https://api.github.com/repos/zhenorzz/goploy').then(response => response.json()).then(data => {
// this.starCount = data.stargazers_count
// this.forkCount = data.forks_count
// })
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
handleNamespaceChange(namespace) {
setNamespace(namespace)
this.$Loading.service({ fullscreen: true })
location.reload()
},
handleSetLanguage(lang) {
this.$i18n.locale = lang
this.$store.dispatch('app/setLanguage', lang)
this.$message.success('Switch language success')
},
async logout() {
await this.$store.dispatch('user/logout')
removeNamespace()
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
},
},
})
</script>
<style lang="scss" scoped>
.navbar {
height: 50px;
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
&-logo {
width: 25px;
float: left;
margin-left: 15px;
margin-top: 11px;
}
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
.breadcrumb-container {
float: left;
}
.right {
float: right;
width: auto;
}
.international {
display: inline-block;
line-height: 50px;
cursor: pointer;
&-icon {
font-size: 18px;
}
}
.github {
display: inline-block;
line-height: 50px;
.github-btn {
display: inline-block;
font: 700 11px/14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
height: 20px;
overflow: hidden;
margin-right: 3px;
position: relative;
top: 5px;
.gh-btn,
.gh-count {
padding: 2px 5px 2px 4px;
color: #333;
text-decoration: none;
text-shadow: 0 1px 0 #fff;
white-space: nowrap;
cursor: pointer;
border-radius: 3px;
}
.gh-btn,
.gh-count,
.gh-ico {
float: left;
}
.gh-btn {
background-color: #eee;
background-image: linear-gradient(to bottom, #fcfcfc 0, #eee 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fcfcfc', endColorstr='#eeeeee', GradientType=0);
background-repeat: no-repeat;
border: 1px solid #d5d5d5;
position: relative;
&:focus,
&:hover {
text-decoration: none;
background-color: #ddd;
background-image: linear-gradient(to bottom, #eee 0, #ddd 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#dddddd', GradientType=0);
border-color: #ccc;
}
&:active {
background-image: none;
background-color: #dcdcdc;
border-color: #b5b5b5;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15);
}
}
.gh-ico {
width: 14px;
height: 14px;
margin-right: 4px;
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjQwcHgiIGhlaWdodD0iNDBweCIgdmlld0JveD0iMTIgMTIgNDAgNDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMTIgMTIgNDAgNDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGZpbGw9IiMzMzMzMzMiIGQ9Ik0zMiAxMy40Yy0xMC41IDAtMTkgOC41LTE5IDE5YzAgOC40IDUuNSAxNS41IDEzIDE4YzEgMC4yIDEuMy0wLjQgMS4zLTAuOWMwLTAuNSAwLTEuNyAwLTMuMiBjLTUuMyAxLjEtNi40LTIuNi02LjQtMi42QzIwIDQxLjYgMTguOCA0MSAxOC44IDQxYy0xLjctMS4yIDAuMS0xLjEgMC4xLTEuMWMxLjkgMC4xIDIuOSAyIDIuOSAyYzEuNyAyLjkgNC41IDIuMSA1LjUgMS42IGMwLjItMS4yIDAuNy0yLjEgMS4yLTIuNmMtNC4yLTAuNS04LjctMi4xLTguNy05LjRjMC0yLjEgMC43LTMuNyAyLTUuMWMtMC4yLTAuNS0wLjgtMi40IDAuMi01YzAgMCAxLjYtMC41IDUuMiAyIGMxLjUtMC40IDMuMS0wLjcgNC44LTAuN2MxLjYgMCAzLjMgMC4yIDQuNyAwLjdjMy42LTIuNCA1LjItMiA1LjItMmMxIDIuNiAwLjQgNC42IDAuMiA1YzEuMiAxLjMgMiAzIDIgNS4xYzAgNy4zLTQuNSA4LjktOC43IDkuNCBjMC43IDAuNiAxLjMgMS43IDEuMyAzLjVjMCAyLjYgMCA0LjYgMCA1LjJjMCAwLjUgMC40IDEuMSAxLjMgMC45YzcuNS0yLjYgMTMtOS43IDEzLTE4LjFDNTEgMjEuOSA0Mi41IDEzLjQgMzIgMTMuNHoiLz48L3N2Zz4=);
background-size: 100% 100%;
background-repeat: no-repeat;
}
.gh-count {
position: relative;
display: none;
margin-left: 4px;
background-color: #fafafa;
border: 1px solid #d4d4d4;
z-index: 1;
display: block;
&:focus,
&:hover {
color: #4183c4;
}
&:after,
&:before {
content: '';
position: absolute;
display: inline-block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
}
&:before {
top: 50%;
left: -2px;
margin-top: -3px;
border-width: 2px 2px 2px 0;
border-right-color: #fafafa;
}
&:after {
top: 50%;
left: -3px;
z-index: -1;
margin-top: -4px;
border-width: 3px 3px 3px 0;
border-right-color: #d4d4d4;
}
}
}
}
.user-menu {
float: right;
height: 100%;
&:focus {
outline: none;
}
&:hover {
background-color: #f5f7fa;
}
.user-container {
padding: 0 20px;
height: 50px;
line-height: 0;
.user-wrapper {
position: relative;
cursor: pointer;
}
.avatar-box {
height: 40px;
width: 40px;
line-height: 40px;
border-radius: 50%;
text-align: center;
color: #fff;
font-size: 16px;
}
.user-name {
margin-top: 4px;
font-size: 16px;
font-weight: 900;
color: #9d9d9d;
}
.user-title {
font-size: 13px;
padding-top: 3px;
color: #9d9d9d;
}
}
}
}
</style>

View File

@ -0,0 +1,68 @@
import { reactive, onUnmounted } from 'vue'
import { parseTime } from '@/utils'
export default function useDateTransform() {
const timeExchange = reactive({
date: parseTime(new Date().getTime()),
timestamp: '',
timer: setInterval(() => {
timeExchange.placeholder = String(Math.round(Date.now() / 1000))
}, 1000),
placeholder: String(Math.round(Date.now() / 1000)),
})
const dateExchange = reactive({
date: '',
timestamp: Math.round(Date.now() / 1000),
timer: setInterval(() => {
dateExchange.placeholder = parseTime(new Date().getTime())
}, 1000),
placeholder: parseTime(new Date().getTime()),
})
const timestamp = (value: string) => {
let ts = 0
switch (value) {
case 'now':
ts = Math.round(Date.now() / 1000)
break
case 'today':
ts = Math.round(
new Date(new Date().setHours(0, 0, 0, 0)).getTime() / 1000
)
break
case 'm1d':
ts =
timeExchange.timestamp !== ''
? parseInt(timeExchange.timestamp) - 86400
: Math.round(Date.now() / 1000) - 86400
break
case 'p1d':
ts =
timeExchange.timestamp !== ''
? parseInt(timeExchange.timestamp) + 86400
: Math.round(Date.now() / 1000) + 86400
break
default:
ts = Math.round(Date.now() / 1000)
}
timeExchange.timestamp = String(ts)
timeExchange.date = parseTime(ts)
}
const timestampToDate = () => {
timeExchange.date = parseTime(Number(timeExchange.timestamp))
}
const dateToTimestamp = () => {
dateExchange.timestamp = new Date(dateExchange.date).getTime() / 1000
}
onUnmounted(() => {
clearTimeout(timeExchange.timer)
clearTimeout(dateExchange.timer)
})
return {
timeExchange,
dateExchange,
timestamp,
timestampToDate,
dateToTimestamp,
}
}

View File

@ -0,0 +1,45 @@
import { reactive } from 'vue'
export default function useRGBTransform() {
const cHexExchange = reactive({
hex: '',
rgb: '',
})
const rgbExchange = reactive({
hex: '',
rgb: '',
})
const hexToRGB = () => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(
cHexExchange.hex
)
if (result) {
const r = parseInt(result[1], 16)
const g = parseInt(result[2], 16)
const b = parseInt(result[3], 16)
cHexExchange.rgb = 'rgb(' + r + ', ' + g + ', ' + b + ')'
} else {
cHexExchange.rgb = 'rgb(0, 0, 0)'
}
}
const rgbToHex = () => {
const color = rgbExchange.rgb.replace(/\(|\)|rgb/g, '')
const rgb = color.split(',')
const r = parseInt(rgb[0])
const g = parseInt(rgb[1])
const b = parseInt(rgb[2])
const hex =
'#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
rgbExchange.hex = hex.toLocaleUpperCase()
}
return {
cHexExchange,
rgbExchange,
hexToRGB,
rgbToHex,
}
}

View File

@ -0,0 +1,39 @@
import { reactive } from 'vue'
export default function useRandomPWD() {
const password = reactive({
checkbox: [1, 2, 3],
length: 8,
text: '',
})
const createPassword = () => {
let randArr: string[] = []
for (const num of password.checkbox) {
if (num === 1) {
for (let i = 0; i < 26; i++) {
randArr.push(String.fromCharCode(65 + i))
}
} else if (num === 2) {
for (let i = 0; i < 26; i++) {
randArr.push(String.fromCharCode(97 + i))
}
} else if (num === 3) {
for (let i = 0; i < 10; i++) {
randArr.push(i)
}
} else {
randArr = randArr.concat(['!', '@', '#', '$', '%', '^', '&', '*'])
}
}
let tmpPWD = ''
for (let i = 0; i < password.length; i++) {
tmpPWD += randArr[Math.floor(Math.random() * randArr.length)]
}
password.text = tmpPWD
}
return {
password,
createPassword,
}
}

View File

@ -0,0 +1,14 @@
import { reactive, computed } from 'vue'
export default function useUnicode() {
const unicode = reactive({
escape: '',
})
const unicodeUnescapeStr = computed(() =>
unescape(unicode.escape.replace(/\\u/g, '%u'))
)
return {
unicode,
unicodeUnescapeStr,
}
}

View File

@ -0,0 +1,443 @@
<template>
<div class="fab">
<el-popover placement="right-end" trigger="hover" popper-class="fab-popper">
<div style="margin: 0 10px">
<el-button
type="text"
size="medium"
@click="showTransformDialog('time')"
>
Date Transform
</el-button>
<el-button
type="text"
size="medium"
@click="showTransformDialog('password')"
>
Random PWD
</el-button>
<el-button
type="text"
size="medium"
@click="showTransformDialog('unicode')"
>
Unicode
</el-button>
<el-button
type="text"
size="medium"
@click="showTransformDialog('decodeURI')"
>
DecodeURI
</el-button>
<div>
<el-button
type="text"
size="medium"
@click="showTransformDialog('md5')"
>
MD5
</el-button>
</div>
<el-button
type="text"
size="medium"
@click="showTransformDialog('cron')"
>
Crontab
</el-button>
<el-button
type="text"
size="medium"
@click="showTransformDialog('qrcode')"
>
QRcode
</el-button>
<el-button
type="text"
size="medium"
@click="showTransformDialog('byte')"
>
Byte Transform
</el-button>
<el-button
type="text"
size="medium"
@click="showTransformDialog('color')"
>
Color Transform
</el-button>
</div>
<template #reference>
<div class="fab-cell">
<i class="el-icon-s-cooperation fab-icon" />
</div>
</template>
</el-popover>
<el-dialog
v-model="transformVisible"
width="600px"
append-to-body
:close-on-click-modal="false"
>
<el-row class="transform-content">
<el-row v-show="transformType === 'time'">
<el-button
style="margin-left: 80px"
type="primary"
@click="timestamp('now')"
>
{{ $t('now') }}
</el-button>
<el-button type="primary" @click="timestamp('today')">
{{ $t('today') }}
</el-button>
<el-button type="primary" @click="timestamp('m1d')">
{{ $t('m1d') }}
</el-button>
<el-button type="primary" @click="timestamp('p1d')">
{{ $t('p1d') }}
</el-button>
<el-row style="margin-top: 10px" type="flex" align="middle">
<span style="width: 70px; font-size: 14px; margin-right: 10px">
Timestamp
</span>
<el-input
v-model="timeExchange.timestamp"
style="width: 200px"
:placeholder="timeExchange.placeholder"
clearable
@keyup.enter="timestampToDate"
/>
<el-button type="primary" @click="timestampToDate">>></el-button>
<el-input v-model="timeExchange.date" style="width: 200px" />
</el-row>
<el-row style="margin-top: 10px" type="flex" align="middle">
<span style="width: 70px; font-size: 14px; margin-right: 10px">
Date
</span>
<el-input
v-model="dateExchange.date"
style="width: 200px"
:placeholder="dateExchange.placeholder"
clearable
@keyup.enter="dateToTimestamp"
/>
<el-button type="primary" @click="dateToTimestamp">>></el-button>
<el-input v-model="dateExchange.timestamp" style="width: 200px" />
</el-row>
</el-row>
<el-row v-show="transformType === 'password'">
<el-checkbox-group v-model="password.checkbox">
<el-checkbox :label="1">A-Z</el-checkbox>
<el-checkbox :label="2">a-z</el-checkbox>
<el-checkbox :label="3">0-9</el-checkbox>
<el-checkbox :label="4">!@#$%^&*</el-checkbox>
</el-checkbox-group>
<el-row style="margin-top: 10px" type="flex" align="middle">
<span style="width: 60px; font-size: 14px; margin-right: 5px">
Length
</span>
<el-input-number
v-model="password.length"
:min="1"
placeholder="Please enter the password length"
/>
<el-button type="primary" @click="createPassword">Gen</el-button>
</el-row>
<el-input :value="password.text" style="margin-top: 10px" readonly />
</el-row>
<el-row v-show="transformType === 'unicode'">
<el-input
v-model="unicode.escape"
type="textarea"
:autosize="{ minRows: 2 }"
placeholder="Please enter unescaped unicode encoding"
/>
<el-input
:value="unicodeUnescapeStr"
style="margin-top: 10px"
type="textarea"
:autosize="{ minRows: 2 }"
readonly
/>
</el-row>
<el-row v-show="transformType === 'decodeURI'">
<el-input
v-model="decodeURI.escape"
type="textarea"
:autosize="{ minRows: 2 }"
placeholder="Please enter unescaped URI"
/>
<el-input
:value="decodeURI.escape ? decodeURI(decodeURI.escape) : ''"
style="margin-top: 10px"
type="textarea"
:autosize="{ minRows: 2 }"
readonly
/>
</el-row>
<el-row v-show="transformType === 'md5'">
<el-input
v-model="md5.text"
type="textarea"
:autosize="{ minRows: 3 }"
/>
<el-input
:value="hashByMD5(md5.text)"
style="margin-top: 10px"
readonly
/>
</el-row>
<el-row v-show="transformType === 'cron'">
<el-input
v-model="cron.expression"
placeholder="* * * * ?"
style="width: 450px; margin-bottom: 10px"
/>
<el-button type="primary" @click="crontabTranslate">>></el-button>
<el-row style="margin-left: 5px">{{ cron.chinese }}</el-row>
<pre>
* * * * *
- - - - -
| | | | |
| | | | +----- Week (0 - 7) (0 for sunday)
| | | +---------- Month (1 - 12)
| | +--------------- Day (1 - 31)
| +-------------------- Hour (0 - 23)
+------------------------- Minute (0 - 59)
</pre>
<el-row style="padding: 0 5px">
在以上各个字段中还可以使用以下特殊字符
<p>
星号( *
)代表所有可能的值例如month字段如果是星号则表示在满足其它字段的制约条件后每月都执行该命令操作
</p>
<p>
逗号( , )可以用逗号隔开的值指定一个列表范围例如"1,2,5,7,8,9"
</p>
<p>
中杠( -
)可以用整数之间的中杠表示一个整数范围例如"2-6"表示"2,3,4,5,6"
</p>
<p>
正斜线( /
)可以用正斜线指定时间的间隔频率例如"0-23/2"表示每两小时执行一次同时正斜线可以和星号一起使用例如*/10minute
</p>
</el-row>
</el-row>
<el-row v-show="transformType === 'qrcode'">
<el-input
v-model="qrcode.text"
type="textarea"
:autosize="{ minRows: 2 }"
/>
<el-row style="margin-top: 10px" type="flex" align="middle">
<span style="width: 30px; font-size: 14px; margin-right: 10px">
Size
</span>
<el-input-number v-model="qrcode.width" />
</el-row>
<VueQrcode
class="text-align:center"
:value="qrcode.text"
:options="{ width: qrcode.width }"
/>
</el-row>
<el-row v-show="transformType === 'byte'" type="flex" align="middle">
<span style="width: 40px; font-size: 14px; margin-right: 10px">
Byte
</span>
<el-input
v-model="bytes"
style="width: 130px"
@keyup.enter="bytesToHumanSize"
/>
<el-select v-model="bytesUnit" style="width: 70px">
<el-option :value="1" label="B" />
<el-option :value="1 * 1024" label="KB" />
<el-option :value="1024 * 1024" label="MB" />
</el-select>
<el-button type="primary" @click="bytesToHumanSize">>></el-button>
<el-input v-model="humanSize" style="width: 200px" />
</el-row>
<el-row v-show="transformType === 'color'">
<el-row type="flex" align="middle">
<span style="width: 40px; font-size: 14px; margin-right: 10px">
HEX
</span>
<el-input
v-model="cHexExchange.hex"
style="width: 200px"
placeholder="#FFFFFF"
clearable
@keyup.enter="hexToRGB"
/>
<el-button type="primary" @click="hexToRGB">>></el-button>
<el-input v-model="cHexExchange.rgb" style="width: 200px" />
</el-row>
<el-row style="margin-top: 10px" type="flex" align="middle">
<span style="width: 40px; font-size: 14px; margin-right: 10px">
RGB
</span>
<el-input
v-model="rgbExchange.rgb"
style="width: 200px"
placeholder="(255,255,255)"
clearable
@keyup.enter="rgbToHex"
/>
<el-button type="primary" @click="rgbToHex">>></el-button>
<el-input v-model="rgbExchange.hex" style="width: 200px" />
</el-row>
</el-row>
</el-row>
</el-dialog>
</div>
</template>
<script>
import VueQrcode from '@chenfengyuan/vue-qrcode'
import cronstrue from 'cronstrue/i18n'
import { humanSize } from '@/utils'
import { md5 as hashByMD5 } from '@/utils/md5'
import { defineComponent } from 'vue'
import useDateTransform from './composables/useDateTransform'
import useRandomPWD from './composables/useRandomPWD'
import useUnicode from './composables/useUnicode'
import useRGBTransform from './composables/useRGBTransform'
export default defineComponent({
components: {
VueQrcode,
},
setup() {
const {
timeExchange,
dateExchange,
timestamp,
timestampToDate,
dateToTimestamp,
} = useDateTransform()
const { password, createPassword } = useRandomPWD()
const { unicode, unicodeUnescapeStr } = useUnicode()
const { cHexExchange, rgbExchange, hexToRGB, rgbToHex } = useRGBTransform()
return {
timeExchange,
dateExchange,
timestamp,
timestampToDate,
dateToTimestamp,
password,
createPassword,
unicode,
unicodeUnescapeStr,
cHexExchange,
rgbExchange,
hexToRGB,
rgbToHex,
}
},
data() {
return {
transformVisible: false,
transformType: '',
qrcode: {
text: 'https://github.com/zhenorzz/goploy',
width: 200,
},
decodeURI: {
escape: '',
},
md5: {
text: '',
},
cron: {
expression: '',
chinese: '',
},
bytes: '',
bytesUnit: 1,
humanSize: '',
}
},
methods: {
hashByMD5,
showTransformDialog(type) {
this.transformVisible = true
this.transformType = type
},
bytesToHumanSize() {
this.humanSize = humanSize(this.bytes * this.bytesUnit)
},
crontabTranslate() {
try {
this.cron.chinese = cronstrue.toString(this.cron.expression, {
locale: 'zh_CN',
})
} catch (error) {
this.$message.error(error)
}
},
},
})
</script>
<style lang="scss" scoped>
@import '@/styles/mixin.scss';
.fab {
z-index: 20;
position: fixed;
right: 0;
bottom: 40px;
width: 36px;
border-radius: 4px 0 0 4px;
background-color: #fff;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.18);
text-align: center;
&-cell {
display: block;
position: relative;
width: 36px;
height: 36px;
transform: translateZ(0);
color: #999;
cursor: pointer;
}
&-icon {
line-height: 36px;
color: #999;
cursor: pointer;
width: 20px;
height: 20px;
}
}
.transform {
&-content {
max-height: 500px;
overflow-y: auto;
@include scrollBar();
}
}
</style>
<style lang="scss">
.fab-popper {
padding: 0;
min-width: 120px !important;
.el-button {
padding: 0;
margin: 4px 0;
color: #606266;
min-height: 20px;
}
.el-button + .el-button {
margin-left: 0px;
}
}
</style>

View File

@ -0,0 +1,27 @@
import { defineComponent } from 'vue'
export default defineComponent({
computed: {
device() {
return this.$store.state.app.device
},
},
mounted() {
// In order to fix the click on menu on the ios device will trigger the mouseleave bug
// https://github.com/PanJiaChen/vue-element-admin/issues/1135
this.fixBugIniOS()
},
methods: {
fixBugIniOS() {
const $subMenu = this.$refs.subMenu
if ($subMenu) {
const handleMouseleave = $subMenu.handleMouseleave
$subMenu.handleMouseleave = (e) => {
if (this.device === 'mobile') {
return
}
handleMouseleave(e)
}
}
},
},
})

View File

@ -0,0 +1,24 @@
<template>
<a v-if="isExternal(to)" :href="to" target="_blank" rel="noopener">
<slot />
</a>
<router-link v-else :to="to">
<slot />
</router-link>
</template>
<script>
import { isExternal } from '@/utils/validate'
import { defineComponent } from 'vue'
export default defineComponent({
props: {
to: {
type: String,
required: true,
},
},
methods: {
isExternal,
},
})
</script>

View File

@ -0,0 +1,117 @@
<template>
<div v-if="!(item.meta && item.meta.hidden)" class="menu-wrapper">
<template
v-if="
hasOneShowingChild(item.children, item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
!item.alwaysShow
"
>
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item
:index="resolvePath(onlyOneChild.path)"
:class="{ 'submenu-title-noDropdown': !isNest }"
>
<svg-icon
v-if="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"
:icon-class="onlyOneChild.meta.icon || item.meta.icon"
/>
<template #title>
{{ $t(`route.${onlyOneChild.meta.title}`) }}
</template>
</el-menu-item>
</app-link>
</template>
<el-submenu
v-else
ref="subMenu"
:index="resolvePath(item.path)"
popper-append-to-body
>
<template #title>
<svg-icon
v-if="item.meta && item.meta.icon"
:icon-class="item.meta.icon"
/>
<span>{{ $t(`route.${item.meta.title}`) }}</span>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</div>
</template>
<script>
import { isExternal } from '@/utils/validate'
import AppLink from './Link.vue'
import FixiOSBug from './FixiOSBug'
import { defineComponent } from 'vue'
import path from 'path-browserify'
export default defineComponent({
name: 'SidebarItem',
components: { AppLink },
mixins: [FixiOSBug],
props: {
// route object
item: {
type: Object,
required: true,
},
isNest: {
type: Boolean,
default: false,
},
basePath: {
type: String,
default: '',
},
},
data() {
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
// TODO: refactor with render function
this.onlyOneChild = null
return {}
},
methods: {
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter((item) => {
if (item.meta && item.meta.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }
return true
}
return false
},
resolvePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
return path.resolve(this.basePath, routePath)
},
},
})
</script>

View File

@ -0,0 +1,50 @@
<template>
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item
v-for="route in permission.routes"
:key="route.path"
:item="route"
:base-path="route.path"
/>
</el-menu>
</el-scrollbar>
</template>
<script>
import { mapState } from 'vuex'
import SidebarItem from './SidebarItem.vue'
import variables from '@/styles/variables.scss'
import { defineComponent } from 'vue'
import { parseExportCSS } from '@/utils'
export default defineComponent({
components: { SidebarItem },
computed: {
...mapState(['app', 'permission']),
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
variables() {
return parseExportCSS(variables)
},
isCollapse() {
return !this.app.sidebar.opened
},
},
})
</script>

View File

@ -0,0 +1,105 @@
<template>
<el-scrollbar
ref="scrollContainer"
:vertical="false"
class="scroll-container"
@wheel.prevent="handleScroll"
>
<slot />
</el-scrollbar>
</template>
<script>
import { defineComponent } from 'vue'
const tagAndTagSpacing = 4 // tagAndTagSpacing
export default defineComponent({
name: 'ScrollPane',
props: {
tagRefs: {
type: Array,
required: true,
},
},
emits: ['scroll'],
data() {
return {
left: 0,
}
},
computed: {
scrollWrapper() {
return this.$refs.scrollContainer.$refs.wrap
},
},
mounted() {
this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
},
beforeUnmount() {
this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
},
methods: {
handleScroll(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40
const $scrollWrapper = this.scrollWrapper
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
},
emitScroll() {
this.$emit('scroll')
},
moveToTarget(currentTag) {
const $container = this.$refs.scrollContainer.$el
const $containerWidth = $container.offsetWidth
const $scrollWrapper = this.scrollWrapper
const tagList = this.tagRefs
let firstTag = null
let lastTag = null
// find first tag and last tag
if (tagList.length > 0) {
firstTag = tagList[0]
lastTag = tagList[tagList.length - 1]
}
if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = 0
} else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
} else {
// find preTag and nextTag
const currentIndex = tagList.findIndex((item) => item === currentTag)
const prevTag = tagList[currentIndex - 1]
const nextTag = tagList[currentIndex + 1]
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft =
nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft =
prevTag.$el.offsetLeft - tagAndTagSpacing
if (
afterNextTagOffsetLeft >
$scrollWrapper.scrollLeft + $containerWidth
) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
}
}
},
},
})
</script>
<style lang="scss" scoped>
.scroll-container {
white-space: nowrap;
position: relative;
overflow: hidden;
width: 100%;
:deep {
.el-scrollbar__bar {
bottom: 0px;
}
.el-scrollbar__wrap {
height: 49px;
}
}
}
</style>

View File

@ -0,0 +1,320 @@
<template>
<div id="tags-view-container" class="tags-view-container">
<scroll-pane
ref="scrollPane"
class="tags-view-wrapper"
:tag-refs="tagRefs"
@scroll="handleScroll"
>
<router-link
v-for="tag in visitedViews"
:ref="setTagRef"
:key="tag.path"
:class="isActive(tag) ? 'active' : ''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span"
class="tags-view-item"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent="openMenu(tag, $event)"
>
{{ $t('route.' + tag.title) }}
<span
v-if="!isAffix(tag)"
class="el-icon-close"
@click.prevent.stop="closeSelectedTag(tag)"
/>
</router-link>
</scroll-pane>
<ul
v-show="visible"
:style="{ left: left + 'px', top: top + 'px' }"
class="contextmenu"
>
<li @click="refreshSelectedTag(selectedTag)">
{{ $t('tagsView.refresh') }}
</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
{{ $t('tagsView.close') }}
</li>
<li @click="closeOthersTags">{{ $t('tagsView.closeOthers') }}</li>
<li @click="closeAllTags(selectedTag)">{{ $t('tagsView.closeAll') }}</li>
</ul>
</div>
</template>
<script>
import ScrollPane from './ScrollPane.vue'
import path from 'path-browserify'
import { defineComponent } from 'vue'
import { mapState } from 'vuex'
import { deepClone } from '@/utils'
export default defineComponent({
components: { ScrollPane },
data() {
return {
visible: false,
top: 0,
left: 0,
selectedTag: {},
affixTags: [],
tagRefs: [],
}
},
computed: {
...mapState({
visitedViews: (state) => state.tagsView.visitedViews,
routes: (state) => state.permission.routes,
}),
},
watch: {
$route() {
this.addTags()
this.moveToCurrentTag()
},
visible(value) {
if (value) {
document.body.addEventListener('click', this.closeMenu)
} else {
document.body.removeEventListener('click', this.closeMenu)
}
},
},
mounted() {
this.initTags()
this.addTags()
},
beforeUpdate() {
this.tagRefs = []
},
methods: {
setTagRef(el) {
if (el) {
this.tagRefs.push(el)
}
},
isActive(route) {
return route.path === this.$route.path
},
isAffix(tag) {
return tag.meta && tag.meta.affix
},
filterAffixTags(routes, basePath = '/') {
let tags = []
routes.forEach((route) => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path)
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta },
})
}
if (route.children) {
const tempTags = this.filterAffixTags(route.children, route.path)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags]
}
}
})
return tags
},
initTags() {
const affixTags = (this.affixTags = this.filterAffixTags(this.routes))
for (const tag of affixTags) {
// Must have tag name
if (tag.name) {
this.$store.dispatch('tagsView/addVisitedView', tag)
}
}
},
addTags() {
const { name } = this.$route
if (name) {
this.$store.dispatch('tagsView/addView', this.$route)
}
return false
},
moveToCurrentTag() {
this.$nextTick(() => {
for (const tag of this.tagRefs) {
if (tag.to.path === this.$route.path) {
this.$refs.scrollPane.moveToTarget(tag)
// when query is different then update
if (tag.to.fullPath !== this.$route.fullPath) {
this.$store.dispatch('tagsView/updateVisitedView', this.$route)
}
break
}
}
})
},
refreshSelectedTag(view) {
this.$store.dispatch('tagsView/delCachedView', view).then(() => {
const { fullPath } = view
this.$nextTick(() => {
this.$router.replace({
path: '/redirect' + fullPath,
})
})
})
},
closeSelectedTag(view) {
this.$store
.dispatch('tagsView/delView', view)
.then(({ visitedViews }) => {
if (this.isActive(view)) {
this.toLastView(visitedViews, view)
}
})
},
closeOthersTags() {
this.$router.push(this.selectedTag)
this.$store
.dispatch('tagsView/delOthersViews', this.selectedTag)
.then(() => {
this.moveToCurrentTag()
})
},
closeAllTags(view) {
this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
if (this.affixTags.some((tag) => tag.path === view.path)) {
return
}
this.toLastView(visitedViews, view)
})
},
toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
this.$router.push(latestView.fullPath)
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view.name === 'Dashboard') {
// to reload home page
this.$router.replace({ path: '/redirect' + view.fullPath })
} else {
this.$router.push('/')
}
}
},
openMenu(tag, e) {
const menuMinWidth = 105
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
const offsetWidth = this.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const left = e.clientX - offsetLeft + 15 // 15: margin right
if (left > maxLeft) {
this.left = maxLeft
} else {
this.left = left
}
this.top = e.clientY - 30
this.visible = true
this.selectedTag = tag
},
closeMenu() {
this.visible = false
},
handleScroll() {
this.closeMenu()
},
},
})
</script>
<style lang="scss" scoped>
.tags-view-container {
height: 34px;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
.tags-view-wrapper {
.tags-view-item {
display: inline-block;
position: relative;
cursor: pointer;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
background-color: #42b983;
color: #fff;
border-color: #42b983;
&::before {
content: '';
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 2px;
}
}
}
}
.contextmenu {
margin: 0;
background: #fff;
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
}
</style>
<style lang="scss">
//reset element css of el-icon-close
.tags-view-wrapper {
.tags-view-item {
.el-icon-close {
width: 16px;
height: 16px;
vertical-align: 2px;
border-radius: 50%;
text-align: center;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transform-origin: 100% 50%;
&:before {
transform: scale(0.6);
display: inline-block;
vertical-align: -3px;
}
&:hover {
background-color: #b4bccc;
color: #fff;
}
}
}
}
</style>

View File

@ -0,0 +1,5 @@
export { default as Navbar } from './Navbar.vue'
export { default as Sidebar } from './Sidebar/index.vue'
export { default as AppMain } from './AppMain.vue'
export { default as RightMenu } from './RightMenu/index.vue'
export { default as TagsView } from './TagsView/index.vue'

93
web+/src/layout/index.vue Normal file
View File

@ -0,0 +1,93 @@
<template>
<div :class="classObj" class="app-wrapper">
<div
v-if="device === 'mobile' && sidebar.opened"
class="drawer-bg"
@click="handleClickOutside"
/>
<div>
<Navbar />
</div>
<Sidebar class="sidebar-container" />
<div class="main-container">
<TagsView />
<AppMain />
</div>
<RightMenu />
</div>
</template>
<script>
import { Navbar, Sidebar, AppMain, RightMenu, TagsView } from './components'
import ResizeMixin from './mixin/ResizeHandler'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Layout',
components: {
Navbar,
Sidebar,
AppMain,
RightMenu,
TagsView,
},
mixins: [ResizeMixin],
computed: {
sidebar() {
return this.$store.state.app.sidebar
},
device() {
return this.$store.state.app.device
},
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile',
}
},
},
created() {
this.$store.dispatch('websocket/init')
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
},
},
})
</script>
<style lang="scss" scoped>
@import '@/styles/mixin.scss';
@import '@/styles/variables.scss';
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px);
}
.mobile .fixed-header {
width: 100%;
}
</style>

View File

@ -0,0 +1,45 @@
import store from '@/store'
const { body } = document
const WIDTH = 992 // refer to Bootstrap's responsive design
import { defineComponent } from 'vue'
export default defineComponent({
watch: {
$route() {
if (this.device === 'mobile' && this.sidebar.opened) {
store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
},
},
beforeMount() {
window.addEventListener('resize', this.$_resizeHandler)
},
beforeUnmount() {
window.removeEventListener('resize', this.$_resizeHandler)
},
mounted() {
const isMobile = this.$_isMobile()
if (isMobile) {
store.dispatch('app/toggleDevice', 'mobile')
store.dispatch('app/closeSideBar', { withoutAnimation: true })
}
},
methods: {
// use $_ for mixins properties
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
$_isMobile() {
const rect = body.getBoundingClientRect()
return rect.width - 1 < WIDTH
},
$_resizeHandler() {
if (!document.hidden) {
const isMobile = this.$_isMobile()
store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
if (isMobile) {
store.dispatch('app/closeSideBar', { withoutAnimation: true })
}
}
},
},
})

27
web+/src/main.ts Normal file
View File

@ -0,0 +1,27 @@
import { createApp } from 'vue'
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
import '@/styles/index.scss' // global css
import store from '@/store'
import router from '@/router'
import '@/permission'
import i18n from '@/lang' // internationalization
import SvgIcon from '@/components/SvgIcon/index.vue' // svg component
import ElementPlus from 'element-plus'
import 'element-plus/lib/theme-chalk/index.css'
import 'vite-plugin-svg-icons/register' // register svg sprite map
import global from '@/global' // global config
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus, {
i18n: i18n.global.t,
size: 'mini',
})
app.use(router)
app.use(store)
app.use(i18n)
app.component('SvgIcon', SvgIcon)
app.config.globalProperties.$global = global
app.mount('#app')

View File

@ -0,0 +1,16 @@
export default {
data() {
return {
tableHeight: window.innerHeight - 160 // table高度
}
},
mounted() {
try {
const outHeight = document.getElementsByClassName('app-bar')[0].clientHeight + 140 // 除了table外 查询与按钮的高度 + 其他的高度150
const maxHeight = window.innerHeight - outHeight
this.tableHeight = maxHeight
} catch (e) {
console.log('缺少节点app-bar')
}
}
}

69
web+/src/permission.ts Normal file
View File

@ -0,0 +1,69 @@
import router from './router'
import store from './store'
import { ElMessage } from 'element-plus'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { isLogin, logout } from '@/utils/auth' // get token from cookie
import { RouteRecordRaw } from 'vue-router'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login'] // no redirect whitelist
router.beforeEach(async (to) => {
// start progress bar
NProgress.start()
// set page title
document.title = 'Goploy'
// determine whether the user has logged in
if (isLogin()) {
if (to.path === '/login') {
// if is logged in, redirect to the home page
NProgress.done()
return '/'
} else {
// determine whether the user has obtained his permission roles through getInfo
const hasUID = store.state.user.id && store.state.user.id !== 0
if (hasUID) {
return true
} else {
try {
// get user info
// note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
await store.dispatch('user/getInfo')
// generate accessible routes map based on roles
const accessRoutes = await store.dispatch('permission/generateRoutes')
// dynamically add accessible routes
accessRoutes.forEach((route: RouteRecordRaw) =>
router.addRoute(route)
)
// hack method to ensure that addRoutes is complete
// set the replace: true, so the navigation will not leave a history record
return { ...to, replace: true }
} catch (error) {
// remove token and go to login page to re-login
logout()
ElMessage.error(error || 'Has Error')
NProgress.done()
return `/login?redirect=${to.path}`
}
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
return true
} else {
// other pages that do not have permission to access are redirected to the login page.
NProgress.done()
return `/login?redirect=${to.path}`
}
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})

261
web+/src/router/index.ts Normal file
View File

@ -0,0 +1,261 @@
import { RouteRecordRaw, createRouter, createWebHashHistory } from 'vue-router'
/* Layout */
import Layout from '@/layout/index.vue'
/**
* Note: sub-menu only appear when route children.length >= 1
* Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
*
* hidden: true if set true, item will not show in the sidebar(default is false)
* alwaysShow: true if set true, will always show the root menu
* if not set alwaysShow, when item has more than one children route,
* it will becomes nested mode, otherwise not show the root menu
* redirect: noRedirect if set noRedirect will no redirect in the breadcrumb
* name:'router-name' the name is used by <keep-alive> (must set!!!)
* meta : {
roles: ['admin', 'manager', 'group-manager', 'member'] control the page roles (you can set multiple roles)
title: 'title' the name show in sidebar and breadcrumb (recommend set)
icon: 'svg-name' the icon show in the sidebar
breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
}
*/
export const homeRoutes: RouteRecordRaw[] = [
// 预留常量 permission.js 会修改权限的第一条
{ path: '/', redirect: '/user' },
]
/**
* constantRoutes
* a base page that does not have permission requirements
* all roles can be accessed
*/
export const constantRoutes: RouteRecordRaw[] = [
{
path: '/login',
component: () => import('@/views/login/index.vue'),
meta: { hidden: true },
},
{
path: '/redirect',
component: Layout,
meta: { hidden: true },
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/redirect/index.vue'),
},
],
},
{
path: '/404',
component: () => import('@/views/404.vue'),
meta: { hidden: true },
},
{
path: '/user',
component: Layout,
redirect: '/user/profile',
meta: { hidden: true },
children: [
{
path: 'profile',
name: 'UserProfile',
component: () => import('@/views/user/profile.vue'),
meta: { title: 'userProfile' },
},
],
},
]
/**
* asyncRoutes
* the routes that need to be dynamically loaded based on user permission_uri
*/
export const asyncRoutes: RouteRecordRaw[] = [
{
path: '/deploy',
component: Layout,
redirect: '/deploy/index',
meta: {
title: 'deploy',
icon: 'deploy',
},
children: [
{
path: 'index',
name: 'Deploy',
component: () => import('@/views/deploy/index.vue'),
meta: {
title: 'deploy',
icon: 'deploy',
affix: true,
},
},
],
},
{
path: '/toolbox',
component: Layout,
redirect: '/toolbox/json',
meta: {
title: 'toolbox',
icon: 'toolbox',
},
children: [
{
path: 'json',
name: 'JSONFormatter',
component: () => import('@/views/toolbox/json/index.vue'),
meta: {
title: 'json',
icon: 'json',
},
},
],
},
{
path: '/monitor',
component: Layout,
redirect: '/monitor/index',
meta: {
title: 'monitor',
icon: 'monitor',
roles: ['admin', 'manager', 'group-manager'],
},
children: [
{
path: 'index',
name: 'Monitor',
component: () => import('@/views/monitor/index.vue'),
meta: {
title: 'monitor',
icon: 'monitor',
roles: ['admin', 'manager', 'group-manager'],
},
},
],
},
{
path: '/project',
component: Layout,
redirect: '/project/index',
meta: {
title: 'project',
icon: 'project',
roles: ['admin', 'manager', 'group-manager'],
},
children: [
{
path: 'index',
name: 'Project',
component: () => import('@/views/project/index.vue'),
meta: {
title: 'project',
icon: 'project',
roles: ['admin', 'manager', 'group-manager'],
},
},
],
},
{
path: '/server',
component: Layout,
redirect: '/server/index',
meta: {
title: 'server',
icon: 'server',
roles: ['admin', 'manager'],
},
children: [
{
path: 'index',
name: 'Server',
component: () => import('@/views/server/index.vue'),
meta: {
title: 'serverSetting',
icon: 'setting',
roles: ['admin', 'manager'],
},
},
{
path: 'crontab',
name: 'Crontab',
component: () => import('@/views/server/crontab.vue'),
meta: {
title: 'crontab',
icon: 'crontab',
roles: ['admin', 'manager'],
},
},
],
},
{
path: '/namespace',
component: Layout,
redirect: '/namespace/index',
meta: {
title: 'namespace',
icon: 'namespace',
roles: ['admin', 'manager'],
},
children: [
{
path: 'index',
name: 'Namespace',
component: () => import('@/views/namespace/index.vue'),
meta: {
title: 'namespace',
icon: 'namespace',
roles: ['admin', 'manager'],
},
},
],
},
{
path: '/member',
component: Layout,
redirect: '/member/index',
meta: {
title: 'member',
icon: 'user',
roles: ['admin'],
},
children: [
{
path: 'index',
name: 'Member',
component: () => import('@/views/member/index.vue'),
meta: {
title: 'member',
icon: 'user',
roles: ['admin'],
},
},
],
},
// 404 page must be placed at the end !!!
{ path: '/:pathMatch(.*)*', redirect: '/404', meta: { hidden: true } },
]
const router = createRouter({
history: createWebHashHistory(),
scrollBehavior() {
return {
el: '#app',
left: 0,
behavior: 'smooth',
}
},
routes: constantRoutes,
})
export function resetRouter(): void {
router
.getRoutes()
.forEach((route) => route.name && router.removeRoute(route.name))
constantRoutes.forEach((route: RouteRecordRaw) => router.addRoute(route))
}
export default router

9
web+/src/router/shims-vue-router.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
// 是可选的
roles?: string[]
hidden?: boolean
}
}

3
web+/src/shims-env.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
interface ImportMetaEnv {
VITE_APP_BASE_API: string
}

14
web+/src/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<
Record<string, unknown>,
Record<string, unknown>,
unknown
>
export default component
}
declare module 'path-browserify' {
import path from 'path'
export default path
}

13
web+/src/store/index.ts Normal file
View File

@ -0,0 +1,13 @@
import { ModuleTree, createStore, createLogger } from 'vuex'
import { RootState } from './types'
const files = import.meta.globEager('./modules/*/index.ts')
const modules: ModuleTree<RootState> = {}
for (const path in files) {
modules[path.split('/')[2]] = files[path].default
}
const store = createStore({
strict: true,
modules: { ...modules },
plugins: process.env.NODE_ENV !== 'production' ? [createLogger()] : [],
})
export default store

View File

@ -0,0 +1,64 @@
import { Module, MutationTree, ActionTree } from 'vuex'
import { RootState } from '../../types'
import Cookies from 'js-cookie'
import { getLanguage } from '@/lang/index'
import { AppState } from './types'
const state: AppState = {
sidebar: {
opened: Cookies.get('sidebarStatus')
? !!Cookies.get('sidebarStatus')
: true,
withoutAnimation: false,
},
device: 'desktop',
language: getLanguage(),
}
const mutations: MutationTree<AppState> = {
TOGGLE_SIDEBAR: (state) => {
state.sidebar.opened = !state.sidebar.opened
state.sidebar.withoutAnimation = false
if (state.sidebar.opened) {
Cookies.set('sidebarStatus', '1')
} else {
Cookies.set('sidebarStatus', '0')
}
},
CLOSE_SIDEBAR: (state, withoutAnimation: boolean) => {
Cookies.set('sidebarStatus', '0')
state.sidebar.opened = false
state.sidebar.withoutAnimation = withoutAnimation
},
TOGGLE_DEVICE: (state, device: string) => {
state.device = device
},
SET_LANGUAGE: (state, language: string) => {
state.language = language
Cookies.set('language', language)
},
}
const actions: ActionTree<AppState, RootState> = {
toggleSideBar(context) {
context.commit('TOGGLE_SIDEBAR')
},
closeSideBar({ commit }, { withoutAnimation }) {
commit('CLOSE_SIDEBAR', withoutAnimation)
},
toggleDevice({ commit }, device: string) {
commit('TOGGLE_DEVICE', device)
},
setLanguage({ commit }, language: string) {
commit('SET_LANGUAGE', language)
},
}
const app: Module<AppState, RootState> = {
namespaced: true,
state,
mutations,
actions,
}
export default app

View File

@ -0,0 +1,10 @@
export type AppState = {
sidebar: Sidebar
device: string
language: string
}
type Sidebar = {
opened: boolean
withoutAnimation: boolean
}

View File

@ -0,0 +1,82 @@
import { Module, MutationTree, ActionTree } from 'vuex'
import { homeRoutes, asyncRoutes, constantRoutes } from '@/router'
import { RouteRecordRaw } from 'vue-router'
import { getNamespace } from '@/utils/namespace'
import { RootState } from '../../types'
import { PermissionState } from './types'
/**
* Use meta.role to determine if the current user has permission
* @param role
* @param route
*/
function hasPermission(role: string, route: RouteRecordRaw) {
if (route.meta && route.meta.roles) {
return route.meta['roles'].includes(role)
} else {
return true
}
}
/**
* Filter asynchronous routing tables by recursion
* @param routes asyncRoutes
* @param role
*/
export function filterAsyncRoutes(
routes: RouteRecordRaw[],
role: string
): RouteRecordRaw[] {
const res: RouteRecordRaw[] = []
routes.forEach((route) => {
const tmp = { ...route }
if (hasPermission(role, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, role)
}
res.push(tmp)
}
})
return res
}
const state: PermissionState = {
routes: [],
}
const mutations: MutationTree<PermissionState> = {
SET_ROUTES: (state, routes) => {
state.routes = constantRoutes.concat(routes)
},
}
const actions: ActionTree<PermissionState, RootState> = {
generateRoutes({ commit }) {
return new Promise((resolve) => {
const namespace = getNamespace()
let accessRoutes = filterAsyncRoutes(asyncRoutes, namespace.role)
if (
accessRoutes.length !== 0 &&
accessRoutes[0]['children'] &&
accessRoutes[0]['children'].length !== 0
) {
homeRoutes[0].redirect =
accessRoutes[0].path + '/' + accessRoutes[0].children[0].path
}
accessRoutes = homeRoutes.concat(accessRoutes)
commit('SET_ROUTES', accessRoutes)
resolve(accessRoutes)
})
},
}
const permission: Module<PermissionState, RootState> = {
namespaced: true,
state,
mutations,
actions,
}
export default permission

View File

@ -0,0 +1,4 @@
import { RouteRecordRaw } from 'vue-router'
export type PermissionState = {
routes: RouteRecordRaw[]
}

View File

@ -0,0 +1,14 @@
import { Module } from 'vuex'
import { SettingState } from './types'
import { RootState } from '../../types'
const state: SettingState = {
fixedHeader: false,
}
const setting: Module<SettingState, RootState> = {
namespaced: true,
state,
}
export default setting

View File

@ -0,0 +1,3 @@
export type SettingState = {
fixedHeader: boolean
}

View File

@ -0,0 +1,167 @@
import { Module, MutationTree, ActionTree } from 'vuex'
import { RootState } from '../../types'
import { TagsViewState } from './types'
const state: TagsViewState = {
visitedViews: [],
cachedViews: [],
}
const mutations: MutationTree<TagsViewState> = {
ADD_VISITED_VIEW: (state, view) => {
if (state.visitedViews.some((v) => v.path === view.path)) return
state.visitedViews.push(
Object.assign({}, view, {
title: view.meta.title || 'no-name',
matched: {},
})
)
},
ADD_CACHED_VIEW: (state, view) => {
if (state.cachedViews.includes(view.name)) return
if (!view.meta.noCache) {
state.cachedViews.push(view.name)
}
},
DEL_VISITED_VIEW: (state, view) => {
for (const [i, v] of state.visitedViews.entries()) {
if (v.path === view.path) {
state.visitedViews.splice(i, 1)
break
}
}
},
DEL_CACHED_VIEW: (state, view) => {
const index = state.cachedViews.indexOf(view.name)
index > -1 && state.cachedViews.splice(index, 1)
},
DEL_OTHERS_VISITED_VIEWS: (state, view) => {
state.visitedViews = state.visitedViews.filter((v) => {
return v.meta.affix || v.path === view.path
})
},
DEL_OTHERS_CACHED_VIEWS: (state, view) => {
const index = state.cachedViews.indexOf(view.name)
if (index > -1) {
state.cachedViews = state.cachedViews.slice(index, index + 1)
} else {
// if index = -1, there is no cached tags
state.cachedViews = []
}
},
DEL_ALL_VISITED_VIEWS: (state) => {
// keep affix tags
const affixTags = state.visitedViews.filter((tag) => tag.meta.affix)
state.visitedViews = affixTags
},
DEL_ALL_CACHED_VIEWS: (state) => {
state.cachedViews = []
},
UPDATE_VISITED_VIEW: (state, view) => {
for (let v of state.visitedViews) {
if (v.path === view.path) {
v = Object.assign(v, view)
break
}
}
},
}
const actions: ActionTree<TagsViewState, RootState> = {
addView({ dispatch }, view) {
dispatch('addVisitedView', view)
dispatch('addCachedView', view)
},
addVisitedView({ commit }, view) {
commit('ADD_VISITED_VIEW', view)
},
addCachedView({ commit }, view) {
commit('ADD_CACHED_VIEW', view)
},
delView({ dispatch, state }, view) {
return new Promise((resolve) => {
dispatch('delVisitedView', view)
dispatch('delCachedView', view)
resolve({
visitedViews: [...state.visitedViews],
cachedViews: [...state.cachedViews],
})
})
},
delVisitedView({ commit, state }, view) {
return new Promise((resolve) => {
commit('DEL_VISITED_VIEW', view)
resolve([...state.visitedViews])
})
},
delCachedView({ commit, state }, view) {
return new Promise((resolve) => {
commit('DEL_CACHED_VIEW', view)
resolve([...state.cachedViews])
})
},
delOthersViews({ dispatch, state }, view) {
return new Promise((resolve) => {
dispatch('delOthersVisitedViews', view)
dispatch('delOthersCachedViews', view)
resolve({
visitedViews: [...state.visitedViews],
cachedViews: [...state.cachedViews],
})
})
},
delOthersVisitedViews({ commit, state }, view) {
return new Promise((resolve) => {
commit('DEL_OTHERS_VISITED_VIEWS', view)
resolve([...state.visitedViews])
})
},
delOthersCachedViews({ commit, state }, view) {
return new Promise((resolve) => {
commit('DEL_OTHERS_CACHED_VIEWS', view)
resolve([...state.cachedViews])
})
},
delAllViews({ dispatch, state }, view) {
return new Promise((resolve) => {
dispatch('delAllVisitedViews', view)
dispatch('delAllCachedViews', view)
resolve({
visitedViews: [...state.visitedViews],
cachedViews: [...state.cachedViews],
})
})
},
delAllVisitedViews({ commit, state }) {
return new Promise((resolve) => {
commit('DEL_ALL_VISITED_VIEWS')
resolve([...state.visitedViews])
})
},
delAllCachedViews({ commit, state }) {
return new Promise((resolve) => {
commit('DEL_ALL_CACHED_VIEWS')
resolve([...state.cachedViews])
})
},
updateVisitedView({ commit }, view) {
commit('UPDATE_VISITED_VIEW', view)
},
}
const tagsView: Module<TagsViewState, RootState> = {
namespaced: true,
state,
mutations,
actions,
}
export default tagsView

View File

@ -0,0 +1,5 @@
import { RouteLocationNormalizedLoaded } from 'vue-router'
export type TagsViewState = {
visitedViews: RouteLocationNormalizedLoaded[]
cachedViews: RouteLocationNormalizedLoaded[]
}

View File

@ -0,0 +1,96 @@
import { Module, MutationTree, ActionTree } from 'vuex'
import { UserState } from './types'
import { RootState } from '../../types'
import { login, getInfo } from '@/api/user'
import { setLogin, logout } from '@/utils/auth'
import { getNamespace, setNamespace, setNamespaceList } from '@/utils/namespace'
import { resetRouter } from '@/router'
const state: UserState = {
id: 0,
account: '',
name: '',
superManager: 0,
}
const mutations: MutationTree<UserState> = {
SET_ID: (state, id: number) => {
state.id = id
},
SET_ACCOUNT: (state, account: string) => {
state.account = account
},
SET_NAME: (state, name: string) => {
state.name = name
},
SET_SUPER_MANAGER: (state, superManager: number) => {
state.superManager = superManager
},
}
const actions: ActionTree<UserState, RootState> = {
// user login
login(_, userInfo) {
const { account, password } = userInfo
return new Promise((resolve, reject) => {
login({ account: account.trim(), password: password })
.then((response) => {
const { data } = response
let namespace = getNamespace()
if (!namespace) {
namespace = data.namespaceList[data.namespaceList.length - 1]
setNamespace(namespace)
}
setNamespaceList(data.namespaceList)
setLogin('ok')
resolve(response)
})
.catch((error) => {
reject(error)
})
})
},
// get user info
getInfo({ commit }) {
return new Promise((resolve, reject) => {
getInfo()
.then((response) => {
const { data } = response
if (!data) {
reject('Verification failed, please Login again.')
}
const { id, account, name, superManager } = data.userInfo
commit('SET_ID', id)
commit('SET_ACCOUNT', account)
commit('SET_NAME', name)
commit('SET_SUPER_MANAGER', superManager)
resolve(data)
})
.catch((error) => {
reject(error)
})
})
},
// user logout
logout({ commit }) {
return new Promise((resolve) => {
commit('SET_ID', 0)
logout()
resetRouter()
resolve(null)
})
},
}
const setting: Module<UserState, RootState> = {
namespaced: true,
state,
mutations,
actions,
}
export default setting

View File

@ -0,0 +1,6 @@
export type UserState = {
id: number
account: string
name: string
superManager: number
}

View File

@ -0,0 +1,136 @@
import { Module, MutationTree, ActionTree } from 'vuex'
import { WebsocketState } from './types'
import { RootState } from '../../types'
import { parseTime } from '@/utils'
import { ElMessageBox, ElMessage } from 'element-plus'
const state: WebsocketState = {
ws: null,
message: {},
againConnectTime: 0, // 规定时间重连
}
const mutations: MutationTree<WebsocketState> = {
SET_MESSAGE: (state, message) => {
state.message = message
},
SET_WS: (state, ws) => {
state.ws = ws
},
CLOSE_WS: (state) => {
if (state.ws) {
state.ws.close()
}
},
SET_AGAINCONNECTTIME: (state, time) => {
state.againConnectTime = time
},
}
const actions: ActionTree<WebsocketState, RootState> = {
init({ dispatch, commit, state }) {
return new Promise((resolve, reject) => {
const websocket = new WebSocket(
`${location.protocol.replace('http', 'ws')}//${location.host}${
import.meta.env.VITE_APP_BASE_API
}/ws/connect`
)
websocket.onopen = () => {
console.log('websocket连接成功, 时间:' + parseTime(Date.now()))
// 连接成功当成重连次数0 置0
dispatch('setAgainConnectTime', 0)
resolve(websocket)
}
websocket.onerror = (err) => {
console.log('websocket连接发生错误, 时间:' + parseTime(Date.now()))
reject(err)
}
websocket.onmessage = (event) => {
const responseData = JSON.parse(event.data)
console.log(responseData)
dispatch('setMessage', responseData)
}
websocket.onclose = (e) => {
// 1005 主动断开
// websocket close code https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
// 顶号,后台发送关闭帧
dispatch('destory')
if (e.code !== 1005) {
if (state.againConnectTime === 0) {
// 第一次连接断开进来 有一次重连机会
dispatch('setAgainConnectTime', new Date().getTime())
setTimeout(() => {
console.log(
'首次断开,重新主动连接, 时间:' + parseTime(Date.now())
)
dispatch('init')
}, 60000)
} else {
if (new Date().getTime() - state.againConnectTime >= 60000) {
console.log(
'主动连接失败,再次尝试, 时间:' + parseTime(Date.now())
)
// 一分钟后的连接 一次重连机会已用完 还是连接失败,就弹窗询问用户
ElMessageBox.confirm(
'检测到Websocket与服务器断开连接, 请重连!',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(() => {
dispatch('init')
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消重连',
})
})
}
}
}
console.log(
`connection closed (${e.code})${e.reason}, 时间:${parseTime(
Date.now()
)}`
)
}
commit('SET_WS', websocket)
})
},
close({ commit }) {
return new Promise(() => {
commit('CLOSE_WS')
commit('SET_WS', null)
})
},
destory({ commit }) {
commit('SET_WS', null)
},
setAgainConnectTime({ commit }, times) {
commit('SET_AGAINCONNECTTIME', times)
},
setMessage({ commit }, message) {
commit('SET_MESSAGE', message)
},
}
const setting: Module<WebsocketState, RootState> = {
namespaced: true,
state,
mutations,
actions,
}
export default setting

View File

@ -0,0 +1,5 @@
export interface WebsocketState {
ws: WebSocket | null
message: any
againConnectTime: number
}

8
web+/src/store/shims-vuex.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
import { Store } from 'vuex'
import { RootState } from './types'
declare module '@vue/runtime-core' {
// Declare your own store states.
interface ComponentCustomProperties {
$store: Store<RootState>
}
}

15
web+/src/store/types.ts Normal file
View File

@ -0,0 +1,15 @@
import { AppState } from './modules/app/types'
import { SettingState } from './modules/setting/types'
import { PermissionState } from './modules/permission/types'
import { UserState } from './modules/user/types'
import { TagsViewState } from './modules/tagsView/types'
import { WebsocketState } from './modules/websocket/types'
export type RootState = {
app: AppState
setting: SettingState
permission: PermissionState
user: UserState
tagsView: TagsViewState
websocket: WebsocketState
}

View File

@ -0,0 +1,48 @@
// cover some element-ui styles
.el-breadcrumb__inner,
.el-breadcrumb__inner a {
font-weight: 400 !important;
}
.el-upload {
input[type="file"] {
display: none !important;
}
}
.el-upload__input {
display: none;
}
// to fixed https://github.com/ElemeFE/element/issues/2461
.el-dialog {
transform: none;
left: 0;
position: relative;
margin: 0 auto;
}
// refine element ui upload
.upload-container {
.el-upload {
width: 100%;
.el-upload-dragger {
width: 100%;
height: 200px;
}
}
}
// dropdown
.el-dropdown-menu {
a {
display: block
}
}
.el-table th {
display: table-cell!important;
}

View File

@ -0,0 +1,74 @@
@import './variables.scss';
@import './mixin.scss';
@import './transition.scss';
@import './element-ui.scss';
@import './sidebar.scss';
body {
height: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
label {
font-weight: 700;
}
html {
height: 100%;
box-sizing: border-box;
}
#app {
height: 100%;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
a:focus,
a:active {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
div:focus {
outline: none;
}
.clearfix {
&:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
}
// main-container global css
.app-container {
padding: 20px;
}
.app-bar {
width: 100%;
border: 1px solid #EBEEF5;
margin-bottom: 5px;
padding: 10px;
background-color: #f2f6fc;
&-operation {margin-bottom:10px;};
}

Some files were not shown because too many files have changed in this diff Show More