提交
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
||||||
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||||
29
index.html
Normal file
29
index.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>无人机飞行地图系统</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "mapscreen",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@amap/amap-jsapi-loader": "^1.0.1",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"element-plus": "^2.12.0",
|
||||||
|
"vue": "^3.5.24"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"vite": "npm:rolldown-vite@7.2.5"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"vite": "npm:rolldown-vite@7.2.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1032
pnpm-lock.yaml
generated
Normal file
1032
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
1062
src/App.vue
Normal file
1062
src/App.vue
Normal file
File diff suppressed because it is too large
Load Diff
19
src/api/index.js
Normal file
19
src/api/index.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
//demo
|
||||||
|
export function getDemo() {
|
||||||
|
return request({
|
||||||
|
url: '/demo/test/demo',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增岗位
|
||||||
|
export function addPost(data) {
|
||||||
|
return request({
|
||||||
|
url: '/system/post',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
BIN
src/assets/wrj.jpg
Normal file
BIN
src/assets/wrj.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
43
src/components/HelloWorld.vue
Normal file
43
src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
msg: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>{{ msg }}</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<button type="button" @click="count++">count is {{ count }}</button>
|
||||||
|
<p>
|
||||||
|
Edit
|
||||||
|
<code>components/HelloWorld.vue</code> to test HMR
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Check out
|
||||||
|
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||||
|
>create-vue</a
|
||||||
|
>, the official Vue + Vite starter
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Learn more about IDE Support for Vue in the
|
||||||
|
<a
|
||||||
|
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||||
|
target="_blank"
|
||||||
|
>Vue Docs Scaling up Guide</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
8
src/main.js
Normal file
8
src/main.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import './style.css'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(ElementPlus)
|
||||||
|
app.mount('#app')
|
||||||
60
src/plugins/auth.js
Normal file
60
src/plugins/auth.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import store from '@/store'
|
||||||
|
|
||||||
|
function authPermission(permission) {
|
||||||
|
const all_permission = "*:*:*"
|
||||||
|
const permissions = store.getters && store.getters.permissions
|
||||||
|
if (permission && permission.length > 0) {
|
||||||
|
return permissions.some(v => {
|
||||||
|
return all_permission === v || v === permission
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function authRole(role) {
|
||||||
|
const super_admin = "admin"
|
||||||
|
const roles = store.getters && store.getters.roles
|
||||||
|
if (role && role.length > 0) {
|
||||||
|
return roles.some(v => {
|
||||||
|
return super_admin === v || v === role
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// 验证用户是否具备某权限
|
||||||
|
hasPermi(permission) {
|
||||||
|
return authPermission(permission)
|
||||||
|
},
|
||||||
|
// 验证用户是否含有指定权限,只需包含其中一个
|
||||||
|
hasPermiOr(permissions) {
|
||||||
|
return permissions.some(item => {
|
||||||
|
return authPermission(item)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 验证用户是否含有指定权限,必须全部拥有
|
||||||
|
hasPermiAnd(permissions) {
|
||||||
|
return permissions.every(item => {
|
||||||
|
return authPermission(item)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 验证用户是否具备某角色
|
||||||
|
hasRole(role) {
|
||||||
|
return authRole(role)
|
||||||
|
},
|
||||||
|
// 验证用户是否含有指定角色,只需包含其中一个
|
||||||
|
hasRoleOr(roles) {
|
||||||
|
return roles.some(item => {
|
||||||
|
return authRole(item)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 验证用户是否含有指定角色,必须全部拥有
|
||||||
|
hasRoleAnd(roles) {
|
||||||
|
return roles.every(item => {
|
||||||
|
return authRole(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/plugins/cache.js
Normal file
79
src/plugins/cache.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
const sessionCache = {
|
||||||
|
set (key, value) {
|
||||||
|
if (!sessionStorage) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (key != null && value != null) {
|
||||||
|
sessionStorage.setItem(key, value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
get (key) {
|
||||||
|
if (!sessionStorage) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (key == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return sessionStorage.getItem(key)
|
||||||
|
},
|
||||||
|
setJSON (key, jsonValue) {
|
||||||
|
if (jsonValue != null) {
|
||||||
|
this.set(key, JSON.stringify(jsonValue))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getJSON (key) {
|
||||||
|
const value = this.get(key)
|
||||||
|
if (value != null) {
|
||||||
|
return JSON.parse(value)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
remove (key) {
|
||||||
|
sessionStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const localCache = {
|
||||||
|
set (key, value) {
|
||||||
|
if (!localStorage) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (key != null && value != null) {
|
||||||
|
localStorage.setItem(key, value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
get (key) {
|
||||||
|
if (!localStorage) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (key == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return localStorage.getItem(key)
|
||||||
|
},
|
||||||
|
setJSON (key, jsonValue) {
|
||||||
|
if (jsonValue != null) {
|
||||||
|
this.set(key, JSON.stringify(jsonValue))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getJSON (key) {
|
||||||
|
const value = this.get(key)
|
||||||
|
if (value != null) {
|
||||||
|
return JSON.parse(value)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
remove (key) {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* 会话级缓存
|
||||||
|
*/
|
||||||
|
session: sessionCache,
|
||||||
|
/**
|
||||||
|
* 本地缓存
|
||||||
|
*/
|
||||||
|
local: localCache
|
||||||
|
}
|
||||||
79
src/plugins/download.js
Normal file
79
src/plugins/download.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import {Loading, Message} from 'element-ui'
|
||||||
|
import { saveAs } from 'file-saver'
|
||||||
|
import { getToken } from '@/utils/auth'
|
||||||
|
import errorCode from '@/utils/errorCode'
|
||||||
|
import { blobValidate } from "@/utils/ruoyi"
|
||||||
|
|
||||||
|
const baseURL = process.env.VUE_APP_BASE_API
|
||||||
|
let downloadLoadingInstance
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name(name, isDelete = true) {
|
||||||
|
var url = baseURL + "/common/download?fileName=" + encodeURIComponent(name) + "&delete=" + isDelete
|
||||||
|
axios({
|
||||||
|
method: 'get',
|
||||||
|
url: url,
|
||||||
|
responseType: 'blob',
|
||||||
|
headers: { 'Authorization': 'Bearer ' + getToken() }
|
||||||
|
}).then((res) => {
|
||||||
|
const isBlob = blobValidate(res.data)
|
||||||
|
if (isBlob) {
|
||||||
|
const blob = new Blob([res.data])
|
||||||
|
this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
|
||||||
|
} else {
|
||||||
|
this.printErrMsg(res.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
resource(resource) {
|
||||||
|
var url = baseURL + "/common/download/resource?resource=" + encodeURIComponent(resource)
|
||||||
|
axios({
|
||||||
|
method: 'get',
|
||||||
|
url: url,
|
||||||
|
responseType: 'blob',
|
||||||
|
headers: { 'Authorization': 'Bearer ' + getToken() }
|
||||||
|
}).then((res) => {
|
||||||
|
const isBlob = blobValidate(res.data)
|
||||||
|
if (isBlob) {
|
||||||
|
const blob = new Blob([res.data])
|
||||||
|
this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
|
||||||
|
} else {
|
||||||
|
this.printErrMsg(res.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
zip(url, name) {
|
||||||
|
var url = baseURL + url
|
||||||
|
downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", })
|
||||||
|
axios({
|
||||||
|
method: 'get',
|
||||||
|
url: url,
|
||||||
|
responseType: 'blob',
|
||||||
|
headers: { 'Authorization': 'Bearer ' + getToken() }
|
||||||
|
}).then((res) => {
|
||||||
|
const isBlob = blobValidate(res.data)
|
||||||
|
if (isBlob) {
|
||||||
|
const blob = new Blob([res.data], { type: 'application/zip' })
|
||||||
|
this.saveAs(blob, name)
|
||||||
|
} else {
|
||||||
|
this.printErrMsg(res.data)
|
||||||
|
}
|
||||||
|
downloadLoadingInstance.close()
|
||||||
|
}).catch((r) => {
|
||||||
|
console.error(r)
|
||||||
|
Message.error('下载文件出现错误,请联系管理员!')
|
||||||
|
downloadLoadingInstance.close()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
saveAs(text, name, opts) {
|
||||||
|
saveAs(text, name, opts)
|
||||||
|
},
|
||||||
|
async printErrMsg(data) {
|
||||||
|
const resText = await data.text()
|
||||||
|
const rspObj = JSON.parse(resText)
|
||||||
|
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
|
||||||
|
Message.error(errMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
20
src/plugins/index.js
Normal file
20
src/plugins/index.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import tab from './tab'
|
||||||
|
import auth from './auth'
|
||||||
|
import cache from './cache'
|
||||||
|
import modal from './modal'
|
||||||
|
import download from './download'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install(Vue) {
|
||||||
|
// 页签操作
|
||||||
|
Vue.prototype.$tab = tab
|
||||||
|
// 认证对象
|
||||||
|
Vue.prototype.$auth = auth
|
||||||
|
// 缓存对象
|
||||||
|
Vue.prototype.$cache = cache
|
||||||
|
// 模态框对象
|
||||||
|
Vue.prototype.$modal = modal
|
||||||
|
// 下载文件
|
||||||
|
Vue.prototype.$download = download
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/plugins/modal.js
Normal file
83
src/plugins/modal.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { Message, MessageBox, Notification, Loading } from 'element-ui'
|
||||||
|
|
||||||
|
let loadingInstance
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// 消息提示
|
||||||
|
msg(content) {
|
||||||
|
Message.info(content)
|
||||||
|
},
|
||||||
|
// 错误消息
|
||||||
|
msgError(content) {
|
||||||
|
Message.error(content)
|
||||||
|
},
|
||||||
|
// 成功消息
|
||||||
|
msgSuccess(content) {
|
||||||
|
Message.success(content)
|
||||||
|
},
|
||||||
|
// 警告消息
|
||||||
|
msgWarning(content) {
|
||||||
|
Message.warning(content)
|
||||||
|
},
|
||||||
|
// 弹出提示
|
||||||
|
alert(content) {
|
||||||
|
MessageBox.alert(content, "系统提示")
|
||||||
|
},
|
||||||
|
// 错误提示
|
||||||
|
alertError(content) {
|
||||||
|
MessageBox.alert(content, "系统提示", { type: 'error' })
|
||||||
|
},
|
||||||
|
// 成功提示
|
||||||
|
alertSuccess(content) {
|
||||||
|
MessageBox.alert(content, "系统提示", { type: 'success' })
|
||||||
|
},
|
||||||
|
// 警告提示
|
||||||
|
alertWarning(content) {
|
||||||
|
MessageBox.alert(content, "系统提示", { type: 'warning' })
|
||||||
|
},
|
||||||
|
// 通知提示
|
||||||
|
notify(content) {
|
||||||
|
Notification.info(content)
|
||||||
|
},
|
||||||
|
// 错误通知
|
||||||
|
notifyError(content) {
|
||||||
|
Notification.error(content)
|
||||||
|
},
|
||||||
|
// 成功通知
|
||||||
|
notifySuccess(content) {
|
||||||
|
Notification.success(content)
|
||||||
|
},
|
||||||
|
// 警告通知
|
||||||
|
notifyWarning(content) {
|
||||||
|
Notification.warning(content)
|
||||||
|
},
|
||||||
|
// 确认窗体
|
||||||
|
confirm(content) {
|
||||||
|
return MessageBox.confirm(content, "系统提示", {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: "warning",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 提交内容
|
||||||
|
prompt(content) {
|
||||||
|
return MessageBox.prompt(content, "系统提示", {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: "warning",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 打开遮罩层
|
||||||
|
loading(content) {
|
||||||
|
loadingInstance = Loading.service({
|
||||||
|
lock: true,
|
||||||
|
text: content,
|
||||||
|
spinner: "el-icon-loading",
|
||||||
|
background: "rgba(0, 0, 0, 0.7)",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 关闭遮罩层
|
||||||
|
closeLoading() {
|
||||||
|
loadingInstance.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/plugins/tab.js
Normal file
71
src/plugins/tab.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import store from '@/store'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// 刷新当前tab页签
|
||||||
|
refreshPage(obj) {
|
||||||
|
const { path, query, matched } = router.currentRoute
|
||||||
|
if (obj === undefined) {
|
||||||
|
matched.forEach((m) => {
|
||||||
|
if (m.components && m.components.default && m.components.default.name) {
|
||||||
|
if (!['Layout', 'ParentView'].includes(m.components.default.name)) {
|
||||||
|
obj = { name: m.components.default.name, path: path, query: query }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return store.dispatch('tagsView/delCachedView', obj).then(() => {
|
||||||
|
const { path, query } = obj
|
||||||
|
router.replace({
|
||||||
|
path: '/redirect' + path,
|
||||||
|
query: query
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 关闭当前tab页签,打开新页签
|
||||||
|
closeOpenPage(obj) {
|
||||||
|
store.dispatch("tagsView/delView", router.currentRoute)
|
||||||
|
if (obj !== undefined) {
|
||||||
|
return router.push(obj)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 关闭指定tab页签
|
||||||
|
closePage(obj) {
|
||||||
|
if (obj === undefined) {
|
||||||
|
return store.dispatch('tagsView/delView', router.currentRoute).then(({ visitedViews }) => {
|
||||||
|
const latestView = visitedViews.slice(-1)[0]
|
||||||
|
if (latestView) {
|
||||||
|
return router.push(latestView.fullPath)
|
||||||
|
}
|
||||||
|
return router.push('/')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return store.dispatch('tagsView/delView', obj)
|
||||||
|
},
|
||||||
|
// 关闭所有tab页签
|
||||||
|
closeAllPage() {
|
||||||
|
return store.dispatch('tagsView/delAllViews')
|
||||||
|
},
|
||||||
|
// 关闭左侧tab页签
|
||||||
|
closeLeftPage(obj) {
|
||||||
|
return store.dispatch('tagsView/delLeftTags', obj || router.currentRoute)
|
||||||
|
},
|
||||||
|
// 关闭右侧tab页签
|
||||||
|
closeRightPage(obj) {
|
||||||
|
return store.dispatch('tagsView/delRightTags', obj || router.currentRoute)
|
||||||
|
},
|
||||||
|
// 关闭其他tab页签
|
||||||
|
closeOtherPage(obj) {
|
||||||
|
return store.dispatch('tagsView/delOthersViews', obj || router.currentRoute)
|
||||||
|
},
|
||||||
|
// 添加tab页签
|
||||||
|
openPage(title, url, params) {
|
||||||
|
const obj = { path: url, meta: { title: title } }
|
||||||
|
store.dispatch('tagsView/addView', obj)
|
||||||
|
return router.push({ path: url, query: params })
|
||||||
|
},
|
||||||
|
// 修改tab页签
|
||||||
|
updatePage(obj) {
|
||||||
|
return store.dispatch('tagsView/updateVisitedView', obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/style.css
Normal file
80
src/style.css
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/utils/auth.js
Normal file
15
src/utils/auth.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import Cookies from 'js-cookie'
|
||||||
|
|
||||||
|
const TokenKey = 'Admin-Token'
|
||||||
|
|
||||||
|
export function getToken() {
|
||||||
|
return Cookies.get(TokenKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setToken(token) {
|
||||||
|
return Cookies.set(TokenKey, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeToken() {
|
||||||
|
return Cookies.remove(TokenKey)
|
||||||
|
}
|
||||||
6
src/utils/errorCode.js
Normal file
6
src/utils/errorCode.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
'401': '认证失败,无法访问系统资源',
|
||||||
|
'403': '当前操作没有权限',
|
||||||
|
'404': '访问资源不存在',
|
||||||
|
'default': '系统未知错误,请反馈给管理员'
|
||||||
|
}
|
||||||
125
src/utils/request.js
Normal file
125
src/utils/request.js
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { Notification, MessageBox, Message, Loading } from 'element-ui'
|
||||||
|
import store from '@/store'
|
||||||
|
import { getToken } from '@/utils/auth'
|
||||||
|
import errorCode from '@/utils/errorCode'
|
||||||
|
import { tansParams, blobValidate } from "@/utils/ruoyi"
|
||||||
|
import cache from '@/plugins/cache'
|
||||||
|
|
||||||
|
// 是否显示重新登录
|
||||||
|
export let isRelogin = { show: false }
|
||||||
|
|
||||||
|
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
|
||||||
|
// 创建axios实例
|
||||||
|
const service = axios.create({
|
||||||
|
// axios中请求配置有baseURL选项,表示请求URL公共部分
|
||||||
|
baseURL: process.env.VUE_APP_BASE_API,
|
||||||
|
// 超时
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
|
||||||
|
// request拦截器
|
||||||
|
service.interceptors.request.use(config => {
|
||||||
|
// 是否需要设置 token
|
||||||
|
const isToken = (config.headers || {}).isToken === false
|
||||||
|
// 是否需要防止数据重复提交
|
||||||
|
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
|
||||||
|
// if (getToken() && !isToken) {
|
||||||
|
// config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
|
||||||
|
// }
|
||||||
|
// get请求映射params参数
|
||||||
|
if (config.method === 'get' && config.params) {
|
||||||
|
let url = config.url + '?' + tansParams(config.params)
|
||||||
|
url = url.slice(0, -1)
|
||||||
|
config.params = {}
|
||||||
|
config.url = url
|
||||||
|
}
|
||||||
|
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
|
||||||
|
const requestObj = {
|
||||||
|
url: config.url,
|
||||||
|
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
|
||||||
|
time: new Date().getTime()
|
||||||
|
}
|
||||||
|
const requestSize = Object.keys(JSON.stringify(requestObj)).length // 请求数据大小
|
||||||
|
const limitSize = 5 * 1024 * 1024 // 限制存放数据5M
|
||||||
|
if (requestSize >= limitSize) {
|
||||||
|
console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。')
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
const sessionObj = cache.session.getJSON('sessionObj')
|
||||||
|
if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
|
||||||
|
cache.session.setJSON('sessionObj', requestObj)
|
||||||
|
} else {
|
||||||
|
const s_url = sessionObj.url // 请求地址
|
||||||
|
const s_data = sessionObj.data // 请求数据
|
||||||
|
const s_time = sessionObj.time // 请求时间
|
||||||
|
const interval = 1000 // 间隔时间(ms),小于此时间视为重复提交
|
||||||
|
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
|
||||||
|
const message = '数据正在处理,请勿重复提交'
|
||||||
|
console.warn(`[${s_url}]: ` + message)
|
||||||
|
return Promise.reject(new Error(message))
|
||||||
|
} else {
|
||||||
|
cache.session.setJSON('sessionObj', requestObj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}, error => {
|
||||||
|
console.log(error)
|
||||||
|
Promise.reject(error)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
service.interceptors.response.use(res => {
|
||||||
|
// 未设置状态码则默认成功状态
|
||||||
|
const code = res.data.code || 200
|
||||||
|
// 获取错误信息
|
||||||
|
const msg = errorCode[code] || res.data.msg || errorCode['default']
|
||||||
|
// 二进制数据则直接返回
|
||||||
|
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
if (code === 401) {
|
||||||
|
if (!isRelogin.show) {
|
||||||
|
isRelogin.show = true
|
||||||
|
MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
|
||||||
|
isRelogin.show = false
|
||||||
|
store.dispatch('LogOut').then(() => {
|
||||||
|
location.href = '/index'
|
||||||
|
})
|
||||||
|
}).catch(() => {
|
||||||
|
isRelogin.show = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
|
||||||
|
} else if (code === 500) {
|
||||||
|
Message({ message: msg, type: 'error' })
|
||||||
|
return Promise.reject(new Error(msg))
|
||||||
|
} else if (code === 601) {
|
||||||
|
Message({ message: msg, type: 'warning' })
|
||||||
|
return Promise.reject('error')
|
||||||
|
} else if (code !== 200) {
|
||||||
|
Notification.error({ title: msg })
|
||||||
|
return Promise.reject('error')
|
||||||
|
} else {
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
console.log('err' + error)
|
||||||
|
let { message } = error
|
||||||
|
if (message == "Network Error") {
|
||||||
|
message = "后端接口连接异常"
|
||||||
|
} else if (message.includes("timeout")) {
|
||||||
|
message = "系统接口请求超时"
|
||||||
|
} else if (message.includes("Request failed with status code")) {
|
||||||
|
message = "系统接口" + message.substr(message.length - 3) + "异常"
|
||||||
|
}
|
||||||
|
Message({ message: message, type: 'error', duration: 5 * 1000 })
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default service
|
||||||
228
src/utils/ruoyi.js
Normal file
228
src/utils/ruoyi.js
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
/**
|
||||||
|
* 通用js方法封装处理
|
||||||
|
* Copyright (c) 2019 ruoyi
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 日期格式化
|
||||||
|
export function parseTime(time, pattern) {
|
||||||
|
if (arguments.length === 0 || !time) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
|
||||||
|
let date
|
||||||
|
if (typeof time === 'object') {
|
||||||
|
date = time
|
||||||
|
} else {
|
||||||
|
if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
|
||||||
|
time = parseInt(time)
|
||||||
|
} else if (typeof time === 'string') {
|
||||||
|
time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '')
|
||||||
|
}
|
||||||
|
if ((typeof time === 'number') && (time.toString().length === 10)) {
|
||||||
|
time = time * 1000
|
||||||
|
}
|
||||||
|
date = new Date(time)
|
||||||
|
}
|
||||||
|
const formatObj = {
|
||||||
|
y: date.getFullYear(),
|
||||||
|
m: date.getMonth() + 1,
|
||||||
|
d: date.getDate(),
|
||||||
|
h: date.getHours(),
|
||||||
|
i: date.getMinutes(),
|
||||||
|
s: date.getSeconds(),
|
||||||
|
a: date.getDay()
|
||||||
|
}
|
||||||
|
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
|
||||||
|
let value = formatObj[key]
|
||||||
|
// Note: getDay() returns 0 on Sunday
|
||||||
|
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
|
||||||
|
if (result.length > 0 && value < 10) {
|
||||||
|
value = '0' + value
|
||||||
|
}
|
||||||
|
return value || 0
|
||||||
|
})
|
||||||
|
return time_str
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单重置
|
||||||
|
export function resetForm(refName) {
|
||||||
|
if (this.$refs[refName]) {
|
||||||
|
this.$refs[refName].resetFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加日期范围
|
||||||
|
export function addDateRange(params, dateRange, propName) {
|
||||||
|
let search = params
|
||||||
|
search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {}
|
||||||
|
dateRange = Array.isArray(dateRange) ? dateRange : []
|
||||||
|
if (typeof (propName) === 'undefined') {
|
||||||
|
search.params['beginTime'] = dateRange[0]
|
||||||
|
search.params['endTime'] = dateRange[1]
|
||||||
|
} else {
|
||||||
|
search.params['begin' + propName] = dateRange[0]
|
||||||
|
search.params['end' + propName] = dateRange[1]
|
||||||
|
}
|
||||||
|
return search
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回显数据字典
|
||||||
|
export function selectDictLabel(datas, value) {
|
||||||
|
if (value === undefined) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var actions = []
|
||||||
|
Object.keys(datas).some((key) => {
|
||||||
|
if (datas[key].value == ('' + value)) {
|
||||||
|
actions.push(datas[key].label)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (actions.length === 0) {
|
||||||
|
actions.push(value)
|
||||||
|
}
|
||||||
|
return actions.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回显数据字典(字符串、数组)
|
||||||
|
export function selectDictLabels(datas, value, separator) {
|
||||||
|
if (value === undefined || value.length ===0) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value = value.join(",")
|
||||||
|
}
|
||||||
|
var actions = []
|
||||||
|
var currentSeparator = undefined === separator ? "," : separator
|
||||||
|
var temp = value.split(currentSeparator)
|
||||||
|
Object.keys(value.split(currentSeparator)).some((val) => {
|
||||||
|
var match = false
|
||||||
|
Object.keys(datas).some((key) => {
|
||||||
|
if (datas[key].value == ('' + temp[val])) {
|
||||||
|
actions.push(datas[key].label + currentSeparator)
|
||||||
|
match = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!match) {
|
||||||
|
actions.push(temp[val] + currentSeparator)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return actions.join('').substring(0, actions.join('').length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字符串格式化(%s )
|
||||||
|
export function sprintf(str) {
|
||||||
|
var args = arguments, flag = true, i = 1
|
||||||
|
str = str.replace(/%s/g, function () {
|
||||||
|
var arg = args[i++]
|
||||||
|
if (typeof arg === 'undefined') {
|
||||||
|
flag = false
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return arg
|
||||||
|
})
|
||||||
|
return flag ? str : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换字符串,undefined,null等转化为""
|
||||||
|
export function parseStrEmpty(str) {
|
||||||
|
if (!str || str == "undefined" || str == "null") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据合并
|
||||||
|
export function mergeRecursive(source, target) {
|
||||||
|
for (var p in target) {
|
||||||
|
try {
|
||||||
|
if (target[p].constructor == Object) {
|
||||||
|
source[p] = mergeRecursive(source[p], target[p])
|
||||||
|
} else {
|
||||||
|
source[p] = target[p]
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
source[p] = target[p]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造树型结构数据
|
||||||
|
* @param {*} data 数据源
|
||||||
|
* @param {*} id id字段 默认 'id'
|
||||||
|
* @param {*} parentId 父节点字段 默认 'parentId'
|
||||||
|
* @param {*} children 孩子节点字段 默认 'children'
|
||||||
|
*/
|
||||||
|
export function handleTree(data, id, parentId, children) {
|
||||||
|
let config = {
|
||||||
|
id: id || 'id',
|
||||||
|
parentId: parentId || 'parentId',
|
||||||
|
childrenList: children || 'children'
|
||||||
|
}
|
||||||
|
|
||||||
|
var childrenListMap = {}
|
||||||
|
var tree = []
|
||||||
|
for (let d of data) {
|
||||||
|
let id = d[config.id]
|
||||||
|
childrenListMap[id] = d
|
||||||
|
if (!d[config.childrenList]) {
|
||||||
|
d[config.childrenList] = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let d of data) {
|
||||||
|
let parentId = d[config.parentId]
|
||||||
|
let parentObj = childrenListMap[parentId]
|
||||||
|
if (!parentObj) {
|
||||||
|
tree.push(d)
|
||||||
|
} else {
|
||||||
|
parentObj[config.childrenList].push(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tree
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数处理
|
||||||
|
* @param {*} params 参数
|
||||||
|
*/
|
||||||
|
export function tansParams(params) {
|
||||||
|
let result = ''
|
||||||
|
for (const propName of Object.keys(params)) {
|
||||||
|
const value = params[propName]
|
||||||
|
var part = encodeURIComponent(propName) + "="
|
||||||
|
if (value !== null && value !== "" && typeof (value) !== "undefined") {
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
for (const key of Object.keys(value)) {
|
||||||
|
if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
|
||||||
|
let params = propName + '[' + key + ']'
|
||||||
|
var subPart = encodeURIComponent(params) + "="
|
||||||
|
result += subPart + encodeURIComponent(value[key]) + "&"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result += part + encodeURIComponent(value) + "&"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回项目路径
|
||||||
|
export function getNormalPath(p) {
|
||||||
|
if (p.length === 0 || !p || p == 'undefined') {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
let res = p.replace('//', '/')
|
||||||
|
if (res[res.length - 1] === '/') {
|
||||||
|
return res.slice(0, res.length - 1)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证是否为blob格式
|
||||||
|
export function blobValidate(data) {
|
||||||
|
return data.type !== 'application/json'
|
||||||
|
}
|
||||||
296
src/utils/websocket.js
Normal file
296
src/utils/websocket.js
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
/**
|
||||||
|
* WebSocket工具类
|
||||||
|
* 用于实时获取无人机经纬度数据
|
||||||
|
*/
|
||||||
|
class WebSocketClient {
|
||||||
|
constructor(url, options = {}) {
|
||||||
|
this.url = url
|
||||||
|
this.options = {
|
||||||
|
reconnectInterval: options.reconnectInterval || 3000, // 重连间隔
|
||||||
|
maxReconnectAttempts: options.maxReconnectAttempts || 5, // 最大重连次数
|
||||||
|
heartbeatInterval: options.heartbeatInterval || 30000, // 心跳间隔
|
||||||
|
...options
|
||||||
|
}
|
||||||
|
this.ws = null
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
this.heartbeatTimer = null
|
||||||
|
this.reconnectTimer = null
|
||||||
|
this.isManualClose = false
|
||||||
|
this.listeners = {
|
||||||
|
open: [],
|
||||||
|
message: [],
|
||||||
|
error: [],
|
||||||
|
close: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接WebSocket
|
||||||
|
*/
|
||||||
|
connect() {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
console.log('WebSocket已经连接')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.ws = new WebSocket(this.url)
|
||||||
|
this.setupEventHandlers()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WebSocket连接失败:', error)
|
||||||
|
this.handleReconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置事件处理器
|
||||||
|
*/
|
||||||
|
setupEventHandlers() {
|
||||||
|
this.ws.onopen = (event) => {
|
||||||
|
console.log('WebSocket连接成功')
|
||||||
|
this.reconnectAttempts = 0
|
||||||
|
this.isManualClose = false
|
||||||
|
this.startHeartbeat()
|
||||||
|
this.emit('open', event)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data)
|
||||||
|
this.emit('message', data)
|
||||||
|
} catch (error) {
|
||||||
|
// 如果不是JSON格式,直接传递原始数据
|
||||||
|
this.emit('message', event.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket错误:', error)
|
||||||
|
this.emit('error', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onclose = (event) => {
|
||||||
|
console.log('WebSocket连接关闭')
|
||||||
|
this.stopHeartbeat()
|
||||||
|
this.emit('close', event)
|
||||||
|
|
||||||
|
if (!this.isManualClose) {
|
||||||
|
this.handleReconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息
|
||||||
|
*/
|
||||||
|
send(data) {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
if (typeof data === 'object') {
|
||||||
|
this.ws.send(JSON.stringify(data))
|
||||||
|
} else {
|
||||||
|
this.ws.send(data)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('WebSocket未连接,无法发送消息')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭连接
|
||||||
|
*/
|
||||||
|
close() {
|
||||||
|
this.isManualClose = true
|
||||||
|
this.stopHeartbeat()
|
||||||
|
if (this.reconnectTimer) {
|
||||||
|
clearTimeout(this.reconnectTimer)
|
||||||
|
this.reconnectTimer = null
|
||||||
|
}
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close()
|
||||||
|
this.ws = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理重连
|
||||||
|
*/
|
||||||
|
handleReconnect() {
|
||||||
|
if (this.isManualClose) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
|
||||||
|
console.error('WebSocket重连次数已达上限')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reconnectAttempts++
|
||||||
|
console.log(`尝试重连 (${this.reconnectAttempts}/${this.options.maxReconnectAttempts})...`)
|
||||||
|
|
||||||
|
this.reconnectTimer = setTimeout(() => {
|
||||||
|
this.connect()
|
||||||
|
}, this.options.reconnectInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动心跳
|
||||||
|
*/
|
||||||
|
startHeartbeat() {
|
||||||
|
this.stopHeartbeat()
|
||||||
|
this.heartbeatTimer = setInterval(() => {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.send({ type: 'ping' })
|
||||||
|
}
|
||||||
|
}, this.options.heartbeatInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止心跳
|
||||||
|
*/
|
||||||
|
stopHeartbeat() {
|
||||||
|
if (this.heartbeatTimer) {
|
||||||
|
clearInterval(this.heartbeatTimer)
|
||||||
|
this.heartbeatTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加事件监听器
|
||||||
|
*/
|
||||||
|
on(event, callback) {
|
||||||
|
if (this.listeners[event]) {
|
||||||
|
this.listeners[event].push(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除事件监听器
|
||||||
|
*/
|
||||||
|
off(event, callback) {
|
||||||
|
if (this.listeners[event]) {
|
||||||
|
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发事件
|
||||||
|
*/
|
||||||
|
emit(event, data) {
|
||||||
|
if (this.listeners[event]) {
|
||||||
|
this.listeners[event].forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`事件监听器执行错误 (${event}):`, error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取连接状态
|
||||||
|
*/
|
||||||
|
getReadyState() {
|
||||||
|
if (!this.ws) {
|
||||||
|
return WebSocket.CONNECTING
|
||||||
|
}
|
||||||
|
return this.ws.readyState
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已连接
|
||||||
|
*/
|
||||||
|
isConnected() {
|
||||||
|
return this.ws && this.ws.readyState === WebSocket.OPEN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模拟WebSocket数据生成器
|
||||||
|
* 用于在没有真实WebSocket服务器时生成模拟的无人机位置数据
|
||||||
|
*/
|
||||||
|
class MockWebSocketDataGenerator {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.options = {
|
||||||
|
interval: options.interval || 1000, // 数据发送间隔(毫秒)
|
||||||
|
startLat: options.startLat || 39.908823, // 起始纬度(北京天安门)
|
||||||
|
startLng: options.startLng || 116.397470, // 起始经度
|
||||||
|
speed: options.speed || 0.0001, // 移动速度(度/次)
|
||||||
|
...options
|
||||||
|
}
|
||||||
|
this.timer = null
|
||||||
|
this.currentLat = this.options.startLat
|
||||||
|
this.currentLng = this.options.startLng
|
||||||
|
this.direction = Math.random() * 360 // 随机方向(度)
|
||||||
|
this.callbacks = []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始生成数据
|
||||||
|
*/
|
||||||
|
start(callback) {
|
||||||
|
if (this.timer) {
|
||||||
|
this.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
this.callbacks.push(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
// 随机改变方向(模拟飞行路径)
|
||||||
|
if (Math.random() < 0.1) {
|
||||||
|
this.direction = (this.direction + (Math.random() - 0.5) * 30) % 360
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算新位置
|
||||||
|
const rad = (this.direction * Math.PI) / 180
|
||||||
|
this.currentLng += Math.cos(rad) * this.options.speed
|
||||||
|
this.currentLat += Math.sin(rad) * this.options.speed
|
||||||
|
|
||||||
|
// 添加一些随机波动
|
||||||
|
const noiseLng = (Math.random() - 0.5) * 0.00001
|
||||||
|
const noiseLat = (Math.random() - 0.5) * 0.00001
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
type: 'drone_position',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
latitude: this.currentLat + noiseLat,
|
||||||
|
longitude: this.currentLng + noiseLng,
|
||||||
|
altitude: 100 + Math.random() * 50, // 高度(米)
|
||||||
|
speed: 10 + Math.random() * 5, // 速度(m/s)
|
||||||
|
heading: this.direction,
|
||||||
|
battery: 80 + Math.random() * 20 // 电量(%)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callbacks.forEach(cb => {
|
||||||
|
try {
|
||||||
|
cb(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('数据回调执行错误:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, this.options.interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止生成数据
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
if (this.timer) {
|
||||||
|
clearInterval(this.timer)
|
||||||
|
this.timer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置位置
|
||||||
|
*/
|
||||||
|
reset(lat, lng) {
|
||||||
|
this.currentLat = lat || this.options.startLat
|
||||||
|
this.currentLng = lng || this.options.startLng
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { WebSocketClient, MockWebSocketDataGenerator }
|
||||||
|
|
||||||
41
todolist.md
Normal file
41
todolist.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# 任务清单 - 无人机飞行地图系统
|
||||||
|
|
||||||
|
## 任务总览
|
||||||
|
实现全屏高德地图显示和电子围栏功能(绘制和取消)
|
||||||
|
|
||||||
|
## 详细任务列表
|
||||||
|
|
||||||
|
### ✅ 1. 项目准备阶段
|
||||||
|
- [x] 创建技术架构文档
|
||||||
|
- [x] 创建任务清单
|
||||||
|
|
||||||
|
### ✅ 2. 高德地图集成
|
||||||
|
- [x] 在index.html中引入高德地图JS API脚本(通过@amap/amap-jsapi-loader动态加载)
|
||||||
|
- [x] 配置高德地图安全密钥
|
||||||
|
- [x] 安装并配置@amap/amap-jsapi-loader
|
||||||
|
|
||||||
|
### ✅ 3. 地图组件开发
|
||||||
|
- [x] 创建地图容器组件(全屏显示)
|
||||||
|
- [x] 初始化地图实例
|
||||||
|
- [x] 设置地图中心点和缩放级别
|
||||||
|
- [x] 实现地图全屏样式
|
||||||
|
|
||||||
|
### ✅ 4. 电子围栏绘制功能
|
||||||
|
- [x] 实现绘制模式切换
|
||||||
|
- [x] 实现地图点击事件监听
|
||||||
|
- [x] 实现围栏顶点收集
|
||||||
|
- [x] 实现围栏实时预览(多边形显示)
|
||||||
|
- [x] 实现完成绘制功能
|
||||||
|
- [x] 实现围栏数据保存
|
||||||
|
|
||||||
|
### ✅ 5. 电子围栏删除功能
|
||||||
|
- [x] 实现围栏列表管理
|
||||||
|
- [x] 实现单个围栏删除
|
||||||
|
- [x] 实现全部围栏清除
|
||||||
|
- [x] 实现取消当前绘制功能
|
||||||
|
|
||||||
|
### ✅ 6. UI交互优化
|
||||||
|
- [x] 添加绘制/完成/取消按钮
|
||||||
|
- [x] 添加围栏列表显示
|
||||||
|
- [x] 优化用户交互体验
|
||||||
|
|
||||||
7
vite.config.js
Normal file
7
vite.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
})
|
||||||
114
技术架构文档.md
Normal file
114
技术架构文档.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# 技术架构文档 - 无人机飞行地图系统
|
||||||
|
|
||||||
|
## 1. 系统架构设计
|
||||||
|
|
||||||
|
### 1.1 整体架构
|
||||||
|
- **前端框架**: Vue 3 (Composition API)
|
||||||
|
- **地图服务**: 高德地图 JS API
|
||||||
|
- **构建工具**: Vite
|
||||||
|
|
||||||
|
### 1.2 模块划分
|
||||||
|
- **地图容器模块**: 负责地图的初始化和全屏显示
|
||||||
|
- **电子围栏模块**: 负责围栏的绘制、管理和删除
|
||||||
|
|
||||||
|
## 2. 数据结构设计
|
||||||
|
|
||||||
|
### 2.1 电子围栏数据结构
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
id: string, // 围栏唯一标识
|
||||||
|
points: Array<[lng, lat]>, // 围栏顶点坐标数组
|
||||||
|
polygon: AMap.Polygon, // 高德地图多边形对象
|
||||||
|
createdAt: Date // 创建时间
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 状态管理
|
||||||
|
- 使用 Vue 3 的 `ref` 和 `reactive` 管理组件状态
|
||||||
|
- 围栏列表存储在响应式数组中
|
||||||
|
|
||||||
|
## 3. 接口(API)设计
|
||||||
|
|
||||||
|
### 3.1 地图初始化
|
||||||
|
- **函数名**: `initMap()`
|
||||||
|
- **功能**: 初始化高德地图实例
|
||||||
|
- **参数**: 无
|
||||||
|
- **返回值**: Promise<AMap.Map>
|
||||||
|
|
||||||
|
### 3.2 电子围栏绘制
|
||||||
|
- **函数名**: `startDrawingFence()`
|
||||||
|
- **功能**: 开始绘制电子围栏模式
|
||||||
|
- **参数**: 无
|
||||||
|
- **返回值**: void
|
||||||
|
|
||||||
|
- **函数名**: `addFencePoint(lng, lat)`
|
||||||
|
- **功能**: 添加围栏顶点
|
||||||
|
- **参数**:
|
||||||
|
- lng: number (经度)
|
||||||
|
- lat: number (纬度)
|
||||||
|
- **返回值**: void
|
||||||
|
|
||||||
|
- **函数名**: `completeFence()`
|
||||||
|
- **功能**: 完成围栏绘制
|
||||||
|
- **参数**: 无
|
||||||
|
- **返回值**: void
|
||||||
|
|
||||||
|
### 3.3 电子围栏删除
|
||||||
|
- **函数名**: `removeFence(fenceId)`
|
||||||
|
- **功能**: 删除指定围栏
|
||||||
|
- **参数**:
|
||||||
|
- fenceId: string (围栏ID)
|
||||||
|
- **返回值**: void
|
||||||
|
|
||||||
|
- **函数名**: `clearAllFences()`
|
||||||
|
- **功能**: 清除所有围栏
|
||||||
|
- **参数**: 无
|
||||||
|
- **返回值**: void
|
||||||
|
|
||||||
|
## 4. 核心算法与伪代码
|
||||||
|
|
||||||
|
### 4.1 地图初始化流程
|
||||||
|
```
|
||||||
|
1. 加载高德地图JS API
|
||||||
|
2. 创建地图容器DOM元素
|
||||||
|
3. 初始化地图实例,设置中心点和缩放级别
|
||||||
|
4. 绑定地图点击事件
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 电子围栏绘制流程
|
||||||
|
```
|
||||||
|
1. 用户点击"开始绘制"按钮,进入绘制模式
|
||||||
|
2. 用户在地图上点击,添加围栏顶点
|
||||||
|
3. 实时更新围栏预览(多边形)
|
||||||
|
4. 用户点击"完成绘制"或双击完成围栏
|
||||||
|
5. 保存围栏到围栏列表
|
||||||
|
6. 退出绘制模式
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 电子围栏删除流程
|
||||||
|
```
|
||||||
|
1. 用户选择要删除的围栏(通过ID或点击)
|
||||||
|
2. 从地图上移除多边形对象
|
||||||
|
3. 从围栏列表中移除该围栏数据
|
||||||
|
4. 更新地图显示
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 技术实现要点
|
||||||
|
|
||||||
|
### 5.1 高德地图集成
|
||||||
|
- 使用 `@amap/amap-jsapi-loader` 加载高德地图API
|
||||||
|
- 配置安全密钥(key)
|
||||||
|
- 设置地图样式为全屏显示
|
||||||
|
|
||||||
|
### 5.2 电子围栏绘制
|
||||||
|
- 使用高德地图的 `AMap.Polygon` 类创建多边形
|
||||||
|
- 监听地图点击事件,收集坐标点
|
||||||
|
- 支持实时预览围栏形状
|
||||||
|
- 至少需要3个点才能形成围栏
|
||||||
|
|
||||||
|
### 5.3 交互设计
|
||||||
|
- 绘制模式:点击地图添加顶点
|
||||||
|
- 完成绘制:双击或点击完成按钮
|
||||||
|
- 删除围栏:选中围栏后点击删除按钮
|
||||||
|
- 取消绘制:点击取消按钮清除当前绘制
|
||||||
|
|
||||||
Reference in New Issue
Block a user