This commit is contained in:
2025-12-06 23:39:54 +08:00
commit 19b617389b
28 changed files with 3557 additions and 0 deletions

24
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

5
README.md Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

1
public/vite.svg Normal file
View 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

File diff suppressed because it is too large Load Diff

19
src/api/index.js Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,6 @@
export default {
'401': '认证失败,无法访问系统资源',
'403': '当前操作没有权限',
'404': '访问资源不存在',
'default': '系统未知错误,请反馈给管理员'
}

125
src/utils/request.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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 交互设计
- 绘制模式:点击地图添加顶点
- 完成绘制:双击或点击完成按钮
- 删除围栏:选中围栏后点击删除按钮
- 取消绘制:点击取消按钮清除当前绘制