This commit is contained in:
szdot 2023-09-20 21:33:11 +08:00
commit a71741e0a8
43 changed files with 5777 additions and 0 deletions

17
public/index.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

16
src/App.vue Normal file
View File

@ -0,0 +1,16 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
height: 100%;
}
</style>

View File

@ -0,0 +1,75 @@
<template>
<el-breadcrumb separator-class="el-icon-caret-right" class="app-breadcrumb">
<el-breadcrumb-item>
<router-link to="/">主控</router-link>
</el-breadcrumb-item>
<el-breadcrumb-item v-for="(item, index) in breadcrumb" :key="index">
<router-link :to="item.path">{{ item.title }}</router-link>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script>
export default {
name: 'Breadcrumb',
data () {
return {
breadcrumb: []
}
},
mounted () {
this.refreshBreadcrumb()
},
computed: {
/**
* @description: 拿路由列表 并过滤不显示项 和没有权限项
*/
routes () {
return this.$router.options.routes.filter(
item => item.hidden !== true && item.roles.indexOf(this.$store.state.user.power.trim()) >= 0
)
},
/**
* @description: 当前路径
*/
presentPath () {
return this.$route.path
}
},
watch: {
presentPath () {
this.refreshBreadcrumb()
}
},
methods: {
/**
* @description: 刷新面包条数据
*/
refreshBreadcrumb () {
const temp = []
this.routes.forEach(element => {
element.children.forEach(child => {
if (this.presentPath.search(child.path) !== -1) {
temp[0] = { title: element.meta.title, path: element.path }
temp[1] = { title: child.meta.title, path: child.path }
}
})
})
this.breadcrumb = temp
}
}
}
</script>
<style lang="scss" scoped>
@import "@/styles/theme.scss";
.el-breadcrumb * {
font-weight: normal !important;
color: $maintext-color !important;
font-size: 14px;
line-height: 50px;
cursor: text;
}
</style>

View File

@ -0,0 +1,530 @@
<template>
<div class="tab-container">
<el-button size="medium" type="primary" :class="activeIndex === index ? 'butIconGroupBG' : ''" class="butIconGroup"
v-for="(item, index) in controlItems" :key="index" @click="toggleContent(index)">
<i :class="item.icon" class="iconfont f-s-35"></i>
<div class="m-t-5 fb">{{ item.title }}</div>
</el-button>
<!-- 子内容菜单 -->
<div class="content" :class="{ 'active': activeIndex !== null }">
<!-- 订单任务 -->
<div v-if="activeIndex === 0" class="contentBox">
<!-- 标题 -->
<div class="clearB m-b-10 fb f-s-16 contentTit">
<i class="iconfont icon-dingdanguanli f-s-22 m-r-5"></i>
<span>送餐任务</span>
</div>
<!-- 内容 -->
<el-form label-position="left" ref="questForm" :model="questForm" label-width="80px">
<el-form-item label="订单任务">
<el-select v-model="questForm.id" :filterable="isMobile" placeholder="请选择,也可输入搜索" :disabled="airLock">
<el-option v-for="item in questList" :key="item.id" :label="item.id" :value="item.id">
<span class="l">{{ item.id }}</span>
<span class="l m-l-5">{{ item.name }}</span>
<span class="l m-l-5">{{ item.receive_site_name }}</span>
<span v-for="product in item.products" :key="product.id" class="l m-l-5">{{
product.name
}}-{{ product.pro_buff }}-{{ product.num }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="飞机操作">
<el-button-group>
<el-button size="mini" class="f-s-14" v-if="planeState & 1 && airLock" key="pubBut" type="info"
icon="iconfont el-icon-loading" disabled>
<font class="m-l-5">航线锁定</font>
</el-button>
<el-button size="mini" class="f-s-14" v-if="planeState & 1 && !airLock" type="primary"
icon="f-s-14 iconfont icon-chakanzhihangrizhi" @click="runQuest">
<font class="m-l-5">提交任务</font>
</el-button>
<el-button size="mini" class="f-s-14" v-if="planeState & 2" key="wirteBut" type="info"
icon="f-s-14 iconfont el-icon-loading" disabled>
<font class="m-l-5">航点写入中···</font>
</el-button>
<el-button size="mini" class="f-s-14" v-if="planeState & 4 && !(planeState & 16)" type="warning"
icon="f-s-14 iconfont icon-jiesuo"
@click="publishFun('{setPlaneState:{bit:3,state:1,count:2,param:[1,0]}}')">
<font class="m-l-5">解锁飞机</font>
</el-button>
<el-button size="mini" class="f-s-14" v-if="planeState & 16 && !(planeState & 1) && !(planeState & 2)"
type="success" icon="f-s-14 iconfont icon-yangshi_icon_tongyong_departure"
@click="publishFun('{setPlaneState:{bit:5,state:1}')">
<font class="m-l-5">执行任务</font>
</el-button>
</el-button-group>
</el-form-item>
<el-form-item label="订单操作">
<el-button-group>
<el-button size="mini" class="f-s-14" type="danger" icon="iconfont icon-meiyoudingdan-01" key="celBUt"
@click="reQuest">
<font class="m-l-5">取消</font>
</el-button>
<el-button size="mini" class="f-s-14" type="success" icon="iconfont icon-qiandai" key="bingBut"
@click="overQuest">
<font class="m-l-5">已送达</font>
</el-button>
</el-button-group>
</el-form-item>
</el-form>
</div>
<!-- 飞机操作 -->
<template v-if="activeIndex === 1">
<!-- 标题 -->
<div class="clearB m-b-10 fb f-s-16 contentTit">
<i class="iconfont icon-youxishoubing f-s-22 m-r-5"></i>
<span>飞机控制</span>
</div>
<div class="butIconBox">
<el-button size="medium" type="primary" class="butIcon border"
@click="publishFun('{setPlaneState:{bit:3,state:1,count:2,param:[1,0]}}')">
<i class="iconfont icon-jiesuo f-s-24"></i>
<div class="m-t-5">解锁</div>
</el-button>
<el-button size="medium" type="primary" class="butIcon border"
@click="publishFun('{setPlaneState:{bit:3,state:0,count:2,param:[0,21196]}}')">
<i class="iconfont icon-suoding f-s-24"></i>
<div class="m-t-5">加锁</div>
</el-button>
<el-button size="medium" type="primary" class="butIcon border"
@click="publishFun('{setPlaneState:{bit:6,state:1,count:1,param:[2]}')">
<i class="iconfont icon-yangshi_icon_tongyong_departure f-s-24"></i>
<div class="m-t-5">起飞</div>
</el-button>
<el-button size="medium" type="primary" class="butIcon border"
@click="publishFun('{setPlaneState:{bit:7,state:1}')">
<i class="iconfont icon-fengzheng1 f-s-24"></i>
<div class="m-t-5">悬停</div>
</el-button>
<el-button size="medium" type="primary" class="butIcon border">
<i class="iconfont icon-duandianxufei f-s-24"></i>
<div class="m-t-5">恢复航点</div>
</el-button>
<el-button size="medium" type="primary" class="butIcon border"
@click="publishFun('{setPlaneState:{bit:9,state:1}')">
<i class="iconfont icon-yijianfanhang f-s-24"></i>
<div class="m-t-5">返航</div>
</el-button>
<el-button size="medium" type="primary" class="butIcon border"
@click="publishFun('{setPlaneState:{bit:5,state:1}')">
<i class="iconfont icon-yangshi_icon_tongyong_arriving f-s-24"></i>
<div class="m-t-5">降落</div>
</el-button>
</div>
</template>
<!-- 附加模组操作 -->
<template v-if="activeIndex === 2">
<!-- 标题 -->
<div class="clearB m-b-10 fb f-s-16 contentTit">
<i class="iconfont icon-mianxingdiaogou f-s-22 m-r-5"></i>
<span>挂载仓控制</span>
</div>
<div class="butIconBox">
<el-button size="medium" type="primary" class="butIcon border" @click="publishFun('{hookConteroller:4')">
<i class="iconfont icon-zhongliang f-s-24"></i>
<div class="m-t-5">重量归零</div>
</el-button>
<el-button size="medium" type="primary" class="butIcon border" @click="publishFun('{hookConteroller:0')">
<i class="iconfont icon-xiangshang f-s-24"></i>
<div class="m-t-5">收钩</div>
</el-button>
<el-button size="medium" type="primary" class="butIcon border" @click="publishFun('{hookConteroller:3')">
<i class="iconfont icon-qiyong f-s-24"></i>
<div class="m-t-5">继续</div>
</el-button>
<el-button size="medium" type="primary" class="butIcon border" @click="publishFun('{hookConteroller:2')">
<i class="iconfont icon-xuanting-zanting f-s-24"></i>
<div class="m-t-5">暂停</div>
</el-button>
</div>
<!-- 标题 -->
<div class="clearB m-b-10 fb f-s-16 contentTit">
<i class="iconfont icon-shipinjiankong f-s-22 m-r-5"></i>
<span>摄像头控制</span>
</div>
<div class="butIconBox">
<el-button size="medium" type="primary" class="butIcon border"
@click="publishFun('{cameraController:{item:0,val:0}')">
<i class="iconfont icon-icon-rotation f-s-24"></i>
<div class="m-t-5">一键回中</div>
</el-button>
<el-button size="medium" type="primary" class="butIcon border"
@click="publishFun('{cameraController:{item:2,val:0,yaw:0,pitch:-1}}')">
<i class="iconfont icon-rotateX f-s-24"></i>
<div class="m-t-5">一键俯视</div>
</el-button>
<el-button size="medium" type="primary" class="butIcon border">
<i class="iconfont icon-rotateY f-s-24"></i>
<div class="m-t-5">旋转</div>
</el-button>
<el-button size="medium" type="primary" class="butIcon border">
<i class="iconfont icon-fangda f-s-24"></i>
<div class="m-t-5">焦距</div>
</el-button>
</div>
<!-- 标题 -->
<div class="clearB m-b-10 fb f-s-16 contentTit">
<i class="iconfont icon-tongzhi f-s-22 m-r-5"></i>
<span>喇叭控制</span>
</div>
<div class="butIconBox">
<el-button size="medium" type="primary" class="butIcon border">
<i class="iconfont icon-icon-test f-s-24"></i>
<div class="m-t-5">喊话</div>
</el-button>
</div>
</template>
<!-- 飞机调试 -->
<template v-if="activeIndex === 3">
<!-- 标题 -->
<div class="clearB m-b-10 fb f-s-16 contentTit">
<i class="iconfont icon-tongzhi f-s-22 m-r-5"></i>
<span>飞机调试</span>
</div>
<div class="butIconBox">
<el-button size="medium" type="primary" class="butIcon border">
<i class="iconfont icon-icon-test f-s-24"></i>
<div class="m-t-5">喊话</div>
</el-button>
</div>
</template>
</div>
<!-- end 子内容菜单 -->
</div>
</template>
<script>
import { questAss } from '@/utils/api/table'
import mqtt from '@/utils/mqtt'
export default {
name: 'TabController',
data () {
return {
controlItems: [//
{ title: '任务', icon: 'icon-songcanfuwu' },
{ title: '控制', icon: 'icon-youxishoubing' },
{ title: '扩展', icon: 'icon-linghuokuozhan' },
{ title: '调试', icon: 'icon-banshou_Line' }
],
activeIndex: null, //
tabIsOpen: false, // tab
questForm: { //
id: ''
},
planesId: this.$route.params.id, // id
airLock: false //
}
},
props: {
plane: {
typeof: 'Object',
deep: true
}
},
computed: {
/**
* @description: 终端平台是否是pc
*/
isMobile () {
return this.$store.state.app.isMobile
},
/**
* @description: 飞机状态
*/
planeState () {
const plane = this.plane
return plane ? plane.planeState.state : null
},
/**
* @description: 已付款 已承接 未发货 未发起退款 订单列表
*/
questList () {
const plane = this.plane
return plane ? this.$store.state.questList.filter((item) => item.shop_id === plane.shop_id && item.quest === '1' && item.status === '20' && item.back === '0' && item.runing === null) : []
},
/**
* @description: 已付款 已承接 已发货 订单列表
*/
overQuestList () {
return this.$store.state.questList.filter((item) => item.quest === '1' && item.status === '30' && item.runing != null)
},
/**
* @description: 航线列表
*/
routeList () {
return this.$store.state.routeList
},
/**
* @description: 获取站点列表
*/
siteList () {
return this.$store.state.siteList
}
},
methods: {
questAss,
/**
* @description: 菜单切换 PS:UI
* @param {*} index 序号
*/
toggleContent (index) {
this.activeIndex = this.activeIndex === index ? null : index
if (this.tabIsOpen) {
if (index !== this.activeIndex) {
this.tabIsOpen = false
this.$emit('mapXOffset', -200)
}
} else {
this.tabIsOpen = true
this.$emit('mapXOffset', 200)
}
},
/**
* @description: 发布 mqtt 信息
* @param {*} jsonData {'item':val} // item: questAss setPlaneState getPlaneState resetState chan11 chan22 chan33 chan44 hookConteroller cameraController
*/
publishFun (jsonData) {
if (this.plane) {
const val = jsonData
mqtt.publishFun(`cmd/${this.plane.macadd}`, val)
} else {
this.$message.warning('与飞机通信未接通,请稍后')
}
},
/**
* @description: 执行订单任务
*/
runQuest () {
if (this.questForm.id === '') {
this.$message.error('未选择订单任务!')
return
}
let i
this.questList.map((item) => {
if (this.questForm.id === item.id) {
i = true
/* 插入日志 */
this.$store.dispatch('fetchLog', { content: `${this.plane.name} 开始执行 订单ID${item.id}、叫餐号:${item.food_sn}号。` })
/* 执行写在这里 */
if (item.bind_route === null) { //
this.$message.error('此站点,未绑定任务航点')
return
}
let routeData
try {
const bindRoute = item.bind_route.split(',')
routeData = this.routeList.find(element => element.id === bindRoute[0]).route_data
routeData = JSON.parse(routeData)//
/* 处理声音航点 航点里面的表达式 如$food_sn$ 正则替换成订单对应的字段 */
item.telTail = item.tel.substr(-4)// telTail tel
routeData.tasks.forEach((x, index) => {
if (x.sound) {
const str = this.voiceRouteParse(item, x.sound)
routeData.tasks[index].sound = str//
}
})
routeData = JSON.stringify(routeData)//
} catch (error) {
this.$message.error('操作失败,请重新尝试')
}
this.publishFun(`{"questAss":${routeData}}`)//
this.questAss(item.id, 'status', '30')//
this.$store.dispatch('fetchLockSite', { id: item.receive_site_id, runing: this.plane.id })// 线
}
})
if (i) { return }
this.$message.error('此订单已被申请退款或者订单已经被取消!')
},
/**
* @description: 匹配声音航点字符串 比如$food_sn$ food_sn匹配成 送餐订单里面的对应字段
* @param {*} questItem 送餐的订单 ps:从数据库里拿的 订单信息对象
* @param {*} voiceRouteStr 声音航点的字符串
*/
voiceRouteParse (questItem, voiceRouteStr) {
//
const regex = /\$(.*?)\$/g
// 使replace()
const replacedStr = voiceRouteStr.replace(regex, (match, p1) => {
// matchp1
return questItem[p1] //
})
//
return replacedStr // "4513301115846"
},
/**
* @description: 取消任务
*/
reQuest () {
if (!this.airLock) { // ""
return
}
this.$confirm('确认复位飞机状态,并清除航线的锁定?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.overQuestList.forEach((item) => {
if (item.runing === this.planesId) {
/* 插入日志 */
this.$store.dispatch('fetchLog', { content: `订单ID${item.id},送餐任务取消。` })
/* 执行写在这里 */
this.publishFun('{"resetState":1}')//
this.questAss(item.id, 'status', '20')//
this.$store.dispatch('fetchLockSite', { id: item.receive_site_id, runing: 'null' })// 线
}
})
}).catch(() => {
this.$message.info('取消操作!')
})
},
/**
* @description: 已送达任务
*/
overQuest () {
if (!this.airLock) { // ""
return
}
this.$confirm('确认将订单状态改为已送达吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
//
this.overQuestList.map((item) => {
if (item.runing === this.planesId) { //
/* 插入日志 */
this.$store.dispatch('fetchLog', { content: `订单ID${item.id} 送餐任务已完成。` })
/* 执行写在这里 */
this.publishFun('{"resetState":1}')//
this.questAss(item.id, 'status', '40')//
this.$store.dispatch('fetchLockSite', { id: item.receive_site_id, runing: 'null' })// 线
}
})
}).catch(() => {
this.$message.info('取消操作!')
})
},
/**
* @description: 在地图上绘制航线
*/
makeRouteForMap () {
let bindRoute
let routeData
this.siteList.forEach(item => {
if (item.runing === this.planesId) {
//
if (item.bind_route !== null) { //
bindRoute = item.bind_route.split(',')
this.$store.dispatch('fetchRouteList').then(res => { //
routeData = res.find(element => element.id === bindRoute[0]).route_data
this.$emit('makeRoute', JSON.parse(routeData))
})
} else {
this.$message.error('此站点,未绑定任务航点')
}
}
})
}
},
created () {
if (this.siteList && this.routeList) {
this.airLock = this.siteList.some(item => item.runing === this.planesId)
}
},
watch: {
airLock (val) {
if (val) { // 线
this.makeRouteForMap()
} else { // 线
this.$emit('clearRoute')
}
this.$store.dispatch('fetchQuestList')//
this.$store.dispatch('fetchSiteList')//
},
siteList (val) {
this.airLock = val.some(item => item.runing === this.planesId)
}
}
}
</script>
<style lang="scss" scoped>
@import "@/styles/theme.scss";
.tab-container {
height: 365px;
width: 80px;
display: flex;
flex-direction: column;
align-items: flex-end;
position: fixed;
right: 15px;
top: 50%;
transform: translateY(-50%);
z-index: 1000;
}
.butIconGroup {
color: $maintext-color;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 15px;
padding: 5px;
width: 80px;
height: 80px;
text-align: center;
margin-bottom: 20px;
border: none;
box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.2);
}
.butIconGroupBG {
color: $graylight-color;
background-color: $brand-color;
}
.butIconGroup:last-of-type {
margin-bottom: 0px;
}
.content {
color: $maintext-color;
border-radius: 15px;
height: 100%;
position: fixed;
right: -400px;
top: 50%;
transform: translateY(-50%);
width: 350px;
background-color: rgba(255, 255, 255, 0.8);
padding: 20px;
box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.2);
transition: right 0.3s ease;
}
.content.active {
right: 95px;
}
.contentTit i {
vertical-align: middle;
}
.butIcon {
border-radius: 10px;
padding: 5px;
width: 66px;
height: 66px;
text-align: center;
border: none;
float: left;
margin: 0px !important;
margin-bottom: 15px !important;
margin-right: 15px !important;
}
.butIconBox .butIcon:nth-child(4n) {
margin-right: 0 !important;
}
</style>

467
src/components/MapBox.vue Normal file
View File

@ -0,0 +1,467 @@
<template>
<div id="map">
<slot name="content"></slot>
</div>
</template>
<script>
import mapboxgl from 'mapbox-gl'
import planeIcon from '@/assets/svg/plane.svg'
export default {
name: 'MapBox',
data () {
return {
map: null,
planes: [], //
lngLats: [], // 线
wayLngLats: [], // 线
takeoffLngLats: [], // 线
GoogleRasterStyle: {
name: 'Mapbox Streets',
sprite: this.$store.state.settings.host + '/map/sprite',
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
version: 8,
sources: {
google: {
type: 'raster',
tileSize: 256,
tiles: ['https://sb.im/google-maps/vt?lyrs=s&x={x}&y={y}&z={z}']
}
},
layers: [{ id: 'GoogleRasterLayer', type: 'raster', source: 'google' }]
},
MapBoxglRasterStyle: 'mapbox://styles/mapbox/outdoors-v12',
MapBoxglSatellite: 'mapbox://styles/mapbox/satellite-streets-v12'
}
},
computed: {
defaultLnglat () {
return this.$store.getters['app/getDefaultLngLat']
},
defaultZoom () {
return this.$store.getters['app/getDefaultZoom']
}
},
mounted () {
this.init().then(() => { //
this.map.on('load', () => {
// sprite
//
this.map.setFog({
range: [1.0, 8.0],
color: 'white',
'horizon-blend': 0.01
})
// 线
// this.map.addSource('contours', {
// type: 'vector',
// url: 'mapbox://mapbox.mapbox-terrain-v2'
// })
// this.map.addLayer({
// id: 'contours',
// type: 'line',
// source: 'contours',
// 'source-layer': 'contour',
// layout: {
// 'line-join': 'round',
// 'line-cap': 'round'
// },
// paint: {
// 'line-color': '#ff69b4', // 线
// 'line-width': 1 // 线
// }
// })
// 3
// this.map.addSource('mapbox-dem', {
// type: 'raster-dem',
// url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
// tileSize: 512,
// maxzoom: 14
// })
// this.map.setTerrain({ source: 'mapbox-dem', exaggeration: 1.5 })
})
})
},
methods: {
/**
* @description: 地图初始化
*/
async init () {
// token
mapboxgl.accessToken = 'pk.eyJ1Ijoic3pkb3QiLCJhIjoiY2xhNXpjd3IxMG9leTNubjg3dnFhZm84ZyJ9.ubUTyh6ZR87qyndE5adgvw'
// map
this.map = new mapboxgl.Map({
container: 'map',
style: this.GoogleRasterStyle,
center: this.defaultLnglat,
zoom: this.defaultZoom,
pitch: 45,
bearing: 0,
projection: 'globe'
})
//
this.map.addControl(new mapboxgl.ScaleControl(), 'bottom-right')
this.map.addControl(new mapboxgl.NavigationControl(), 'bottom-right')
},
/**
* @description: 清除地图上的航线
*/
clearRoute () {
// 线
if (this.map.getSource('way_route')) {
this.map.removeLayer('takeoff_layer')
this.map.removeSource('takeoff_route')
this.map.removeLayer('way_route_layer')
this.map.removeLayer('way_route_layer1')
this.map.removeSource('way_route')
this.map.removeLayer('home_point_layer')
this.map.removeSource('home_point')
this.map.removeLayer('takeoff_point_layer')
this.map.removeSource('takeoff_point')
for (let i = 2; i < this.lngLats.length - 1; i++) {
this.map.removeLayer('way_point_layer' + i)
this.map.removeSource('way_point' + i)
}
this.lngLats = []
this.wayLngLats = []
this.takeoffLngLats = []
}
},
/**
* @description: 绘画航线
* @param {*} routeObj 航点文件对象
*/
makeRoute (routeObj) {
// 线
this.clearRoute()
//
routeObj.tasks.forEach((element, index) => {
const lngLat = [element.y, element.x, 100]
this.lngLats.push(lngLat)
if (index > 1 && index < routeObj.taskcount - 1) {
this.wayLngLats.push(lngLat)
}
})
// home
this.goto(this.lngLats[0])
// 线
if (routeObj.tasks[this.lngLats.length - 1].command === 20) {
this.takeoffLngLats.push(this.lngLats[2])
this.takeoffLngLats.push(this.lngLats[1])
this.takeoffLngLats.push(this.lngLats[this.lngLats.length - 2])
} else if (routeObj.tasks[this.lngLats.length - 1].command === 21) {
this.takeoffLngLats.push(this.lngLats[2])
this.takeoffLngLats.push(this.lngLats[1])
}
// 线
this.map.addSource('takeoff_route', {
type: 'geojson',
data: {
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: this.takeoffLngLats
}
}
})
this.map.addLayer({
id: 'takeoff_layer',
type: 'line',
source: 'takeoff_route',
layout: {
'line-join': 'round', // 线
'line-cap': 'round'// 线
},
paint: {
'line-color': '#fff', // 线
'line-width': 2, // 线
'line-opacity': 1.0, // 线
'line-dasharray': [3, 2]// 线
}
})
// 线 线
this.map.addSource('way_route', {
type: 'geojson',
data: {
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: this.wayLngLats
}
}
})
this.map.addLayer({
id: 'way_route_layer1',
type: 'line',
source: 'way_route',
layout: {
'line-join': 'round', // 线
'line-cap': 'round'// 线
},
paint: {
'line-color': '#fff', // 线
'line-width': 4, // 线
'line-opacity': 1.0// 线
}
})
this.map.addLayer({
id: 'way_route_layer',
type: 'line',
source: 'way_route',
layout: {
'line-join': 'round', // 线
'line-cap': 'round'// 线
},
paint: {
'line-color': '#f00', // 线
'line-width': 2, // 线
'line-opacity': 1.0// 线
}
})
// PS:home
this.lngLats.forEach((item, index) => {
// home
if (index === 0) { // home
this.map.addSource('home_point', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: item
},
properties: {
'marker-symbol': 'homePoint'// home
}
}]
}
})
this.map.addLayer({
id: 'home_point_layer',
type: 'symbol',
source: 'home_point',
layout: {
'icon-image': '{marker-symbol}',
'icon-size': 1.5, //
'icon-allow-overlap': true
}
})
} else if (index === 1) { //
let takeoffPoint
// RETURN_TO_LAUNCH 20 LAND 21
if (routeObj.tasks[this.lngLats.length - 1].command === 20) {
takeoffPoint = 'takeoffLandPoint'
} else if (routeObj.tasks[this.lngLats.length - 1].command === 21) {
takeoffPoint = 'takeoffPoint'
}
this.map.addSource('takeoff_point', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: item
},
properties: {
'marker-symbol': takeoffPoint//
}
}]
}
})
this.map.addLayer({
id: 'takeoff_point_layer',
type: 'symbol',
source: 'takeoff_point',
layout: {
'icon-image': '{marker-symbol}',
'icon-size': 1.5, //
'icon-allow-overlap': true
}
})
} else { // waypoint
if (index !== this.lngLats.length - 1) { //
let wayPoint = 'wayPoint'
if (index === this.lngLats.length - 2) { // LAND 21 waypoint
if (routeObj.tasks[this.lngLats.length - 1].command === 21) {
wayPoint = 'wayLandPoint'
}
}
if (routeObj.tasks[index].command === 94) { // command94
wayPoint = 'hookPoint'
}
this.map.addSource('way_point' + index, {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: item
},
properties: {
'marker-symbol': wayPoint
}
}]
}
})
this.map.addLayer({
id: 'way_point_layer' + index,
type: 'symbol',
source: 'way_point' + index,
layout: {
'icon-image': '{marker-symbol}',
'icon-size': 1.5, //
'icon-allow-overlap': true
}
})
}
}
})
},
/**
* @description: 创建飞机轨迹 ps:原理删除之前的轨迹 重新绘制
* @param {arr} coordinatesArray 飞机经纬高度数组
*/
createPathWithArray (coordinatesArray) {
// GeoJSON LineString
const geojson = {
type: 'Feature',
properties: {},
geometry: {
type: 'LineString',
coordinates: coordinatesArray.map(coord => [coord[0], coord[1], coord[2]])
}
}
//
if (this.map.getLayer('path')) {
this.map.getSource('path').setData(geojson)
} else { //
//
if (coordinatesArray.length > 0) {
// 3D
this.map.addLayer({
id: 'path',
type: 'line',
source: {
type: 'geojson',
data: geojson
},
layout: {
'line-cap': 'round',
'line-join': 'round'
},
paint: {
'line-color': 'purple',
'line-width': 1
}
})
}
}
},
/**
* @description: 创建一个飞机
* @param {*} plane 飞机对象
*/
makePlane (plane, index = 0) {
const customIcon = document.createElement('div')
customIcon.className = 'custom-marker' //
customIcon.style.backgroundImage = `url(${planeIcon})` // 使 SVG
customIcon.style.width = '64px' //
customIcon.style.height = '64px' //
// marker
this.planes[index] = new mapboxgl.Marker(customIcon)
.setLngLat([plane.lng, plane.lat])
.setPopup(new mapboxgl.Popup({ offset: 25 }).setHTML('<h3>' + plane.name + '</h3><hr><p>macID:' + plane.macadd + '</p>')) //
.addTo(this.map)
},
/**
* @description: 移除页面上的所有飞机
*/
removePlanes () {
this.planes.forEach(plane => {
plane.remove()
})
this.planes = []
},
/**
* @description: 实时更新经纬度
* @param {obj} lnglat lng经度 lat纬度
* @param {*} index 飞机序号
* @param {*} pathArr 是否创建轨迹
*/
setPlaneLngLat (lnglat, index, pathArr, isflow) {
//
const plane = this.planes[index]
if (plane != null) {
plane.setLngLat([lnglat.lng, lnglat.lat])
}
//
this.createPathWithArray(pathArr)//
//
if (isflow) {
this.map.flyTo({
center: lnglat,
speed: 2,
curve: 1,
easing (t) {
return t
}
})
}
},
/**
* @description: 镜头跳转
* @param {obj} lnglat {lng:lng,lat:lat} 经纬度
*/
goto (lnglat) {
this.map.flyTo({
center: lnglat,
speed: 2,
curve: 1,
easing (t) {
return t
}
})
},
/**
* @description: 屏幕横移
* @param {*} val 正数向左移动 负数向右移动
*/
mapXOffset (val) {
this.map.panBy([val, 0], {
duration: 333 //
})
}
},
beforeDestroy () {
if (this.map) {
this.$store.commit('app/setDefaultLngLat', this.map.getCenter())//
this.$store.commit('app/setDefaultZoom', this.map.getZoom())//
this.map.remove()//
}
}
}
</script>
<style lang="scss" scoped>
#map {
width: 100%;
height: 100%;
overflow: hidden;
}
.custom-marker {
background-size: cover;
cursor: pointer;
}
</style>

View File

@ -0,0 +1,78 @@
<template>
<el-row class="m-15" type="flex" justify="space-between">
<el-col :span="2">
<PublicTag icon="icon-weixing" val="16.88" unit="颗" state="normal" />
</el-col>
<el-col :span="2">
<PublicTag icon="icon-weixing" val="16.88" unit="颗" state="normal" />
</el-col>
<el-col :span="2">
<PublicTag icon="icon-weixing" val="16.88" unit="颗" state="normal" />
</el-col>
<el-col :span="2">
<PublicTag icon="icon-gaodu" val="50" unit="米" state="normal" />
</el-col>
<el-col :span="3">
<HeartTag :heartBeat="planeState.heartBeat" :heartRandom="planeState.heartRandom"
:getPlaneMode="planeState.getPlaneMode" />
</el-col>
<el-col :span="2">
<PublicTag icon="icon-mianxingdiaogou" :val="planeState.state" unit="" state="normal" />
</el-col>
<el-col :span="2">
<PublicTag icon="icon-gaodu" :val="planeState.groundSpeed" unit="米" state="normal" />
</el-col>
<el-col :span="2">
<PublicTag icon="icon-gaodu" val="50" unit="米" state="danger" />
</el-col>
<el-col :span="2">
<PublicTag icon="icon-gaodu" val="50" unit="米" state="danger" />
</el-col>
</el-row>
</template>
<script>
import HeartTag from '@/components/Tag/HeartTag'
import PublicTag from '@/components/Tag/PublicTag'
export default {
name: 'PlaneStatus',
data () {
return {
planesId: this.$route.params.id,
planeState: {}
}
},
components: {
HeartTag,
PublicTag
},
computed: {
plane () {
return this.$store.state.airList.find(plane => plane.id === this.planesId)
}
},
watch: {
plane: {
handler (val) {
this.planeState = val.planeState
},
deep: true
}
},
methods: {},
created () {
if (this.plane) {
this.planeState = this.plane.planeState
}
}
}
</script>
<style lang="scss" scoped>
@import "@/styles/theme.scss";
.el-row {
z-index: 90;
}
</style>

View File

@ -0,0 +1,205 @@
<template>
<div class="p-15">
<el-collapse v-model="activeNames" accordion>
<template v-if="list.length != 0">
<el-collapse-item class="mainFontColor" v-for="(item, index) in list" :key="item.id"
:title="'订单ID' + item.id + ' | 时间:' + parseTime(item.addtime)" :name="item.id">
<el-descriptions direction="vertical" :column="3" border>
<el-descriptions-item label="状态" span="1">{{ orderState(item.status, item.back, item.quest) }}
</el-descriptions-item>
<el-descriptions-item label="订单号" span="2">{{ item.order_sn }}</el-descriptions-item>
<el-descriptions-item label="昵称">{{ item.name }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ item.tel }}</el-descriptions-item>
<el-descriptions-item label="价格">{{ item.price }}</el-descriptions-item>
<el-descriptions-item label="货品总数">{{ item.product_num }}</el-descriptions-item>
<el-descriptions-item label="收货站点">{{ item.receive_site_name }}</el-descriptions-item>
<el-descriptions-item label="取餐号">{{ item.food_sn }}</el-descriptions-item>
<el-descriptions-item label="订单详情" span="3">
<el-table :data="item.products" style="width: 100%">
<el-table-column prop="pid" label="ID" width="60">
</el-table-column>
<el-table-column prop="name" label="名称">
</el-table-column>
<el-table-column prop="pro_buff" label="属性">
</el-table-column>
<el-table-column prop="num" label="数量" width="60">
</el-table-column>
</el-table>
</el-descriptions-item>
<el-descriptions-item :label="item.back == 0 ? '客户备注' : '退货缘由'" span="3">
{{ item.back == 0 ? item.remark != '' ? item.remark : '无' : item.back_remark }}
</el-descriptions-item>
<el-descriptions-item label="操作" span="3">
<el-button-group v-if="item.back == 1">
<el-button type="danger" icon="iconfont icon-zhengque">
<font class="m-l-5">同意退款</font>
</el-button>
<el-button @click="questAss(item.id, 'back', 'zero')" type="warning" icon="iconfont icon-cuowu">
<font class="m-l-5">拒绝退款</font>
</el-button>
</el-button-group>
<el-button-group v-else-if="item.back == 0 && item.quest == 0">
<el-button @click="questAss(item.id, 'quest', '1')" type="primary" icon="iconfont icon-chengjie">
<font class="m-l-5">承接订单</font>
</el-button>
<el-button type="danger" icon="iconfont icon-cuowu">
<font class="m-l-5">取消订单</font>
</el-button>
<el-button type="info" icon="iconfont icon-dayin">
<font class="m-l-5" @click="orderPrint(index)">打印小票</font>
</el-button>
</el-button-group>
<el-button-group v-else-if="item.back == 0 && item.quest == 1 && item.status == 20">
<el-button type="danger" icon="iconfont icon-cuowu">
<font class="m-l-5">取消订单</font>
</el-button>
<el-button type="info" icon="iconfont icon-dayin">
<font class="m-l-5" @click="orderPrint(index)">打印小票</font>
</el-button>
</el-button-group>
</el-descriptions-item>
</el-descriptions>
</el-collapse-item>
</template>
<div v-else class="p-t-60 p-b-60 seatFontColor fc">
<i class="iconfont icon-meiyoushuju f-s-100"></i>
<div>空空如也</div>
</div>
</el-collapse>
</div>
</template>
<script>
import { parseTime } from '@/utils/index'
import { questAss } from '@/utils/api/table'
export default {
data () {
return {
list: [],
activeNames: []
}
},
props: ['fil'],
computed: {
/**
* @description: 获取订单列表 ps:待发货及待收货 并没有发起退款和正在退款状态
*/
questList () {
return this.$store.state.questList
}
},
methods: {
parseTime, //
questAss,
orderState (status, back, quest) {
if (back === 1) {
return '申请退款'
} else {
if (quest === 1) {
return '已确认接单'
} else {
return '未确认订单'
}
}
},
/**
* @description: 通过蓝牙打印机并打印小票
* @param {number} index 订单列表下标索引
*/
orderPrint (index) {
//
// console.log(this.list[index])
// let con
// con = '#' + this.list[index].food_sn + '\n\n'
// this.btPrint(con, '10', '1')
// con = ''
// con += '------------------------\n'
// con += '' + this.list[index].name + '\n'
// const tel = this.list[index].tel.substr(0, 3) + '****' + this.list[index].tel.substr(7)
// con += '' + tel + '\n\n'
// con += '------------------------\n'
// this.btPrint(con, '0', '0')
// con = ''
// con += '' + this.list[index].food_sn + '\n'
// this.btPrint(con, '31', '0')
// con = ''
// con += '' + this.list[index].order_sn + '\n'
// con += '' + this.list[index].receive_site_name + '\n'
// con += '' + this.parseTime(this.list[index].addtime) + '\n\n'
// if (this.list[index].remark !== '') {
// con += '------------------------\n'
// con += this.list[index].remark + '\n\n'
// }
// con += '----------------------------\n'
// this.list[index].products.forEach(element => {
// con += 'ID' + element.pid + '\n'
// con += '' + element.name + '\n'
// if (element.pro_buff !== '') {
// con += ': ' + element.pro_buff + '\n'
// }
// con += '' + element.num + '\n'
// con += '' + element.price + '\n'
// con += '················\n'
// })
// con += '' + this.list[index].product_num + '\n'
// con += '' + this.list[index].price + '\n\n'
// con += '------------------\n'
// this.btPrint(con, '0', '0')
// con = ''
// var data = 'https://flicube.com'
// var align = 1
// var model = 49
// var size = 32
// var eclevel = 50
// BTPrinter.printQRCode(function (data) {
// console.log('Success')
// console.log(data)
// }, function (err) {
// console.log('Error')
// console.log(err)
// }, data, align, model, size, eclevel)
// con += '\n\n\n\n'
// this.btPrint(con, '0', '1')
},
/**
* @description: cordova 打印api接口
* @param {string} str 内容
* @param {string} size 字体大小预设
* @param {string} algin 对齐
*/
btPrint (str, size = '0', align = '0') {
// BTPrinter.printTitle(function (data) {
// console.log('Success')
// console.log(data)
// }, function (err) {
// console.log('Error')
// console.log(err)
// }, str, size, align)// string, size, align
},
/**
* @description: 按父组件的fil参数 条件 显示列表
* @param {*} list 要处理的列表
*/
computeQuestList () {
if (this.fil === 2) {
this.list = this.questList.filter(item => item.back === '1')
} else if (this.fil === 0) {
this.list = this.questList.filter(item => item.back === '0' && item.quest === '0' && item.status === '20')
} else if (this.fil === 1) {
this.list = this.questList.filter(item => item.back === '0' && item.quest === '1' && item.status === '20')
}
}
},
watch: {
questList () {
this.computeQuestList()
}
},
mounted () {
this.computeQuestList()
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,43 @@
<template>
<el-select v-if="adminList.length > 1" v-model="form.shop_id" filterable placeholder="所有客户" @input="updateShopId">
<el-option label="所有客户" value="">
<span class="l">All</span>
<span style="float: right; color: #8492a6; font-size: 13px">所有客户</span>
</el-option>
<el-option v-for="item in adminList" :key="item.id" :label="item.uname" :value="item.shop_id">
<span style="float: left">{{ item.name }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.uname }}</span>
</el-option>
</el-select>
</template>
<script>
export default {
name: 'Selection',
props: {
value: String // prop
},
data () {
return {
form: {
shop_id: ''
}
}
},
computed: {
//
adminList () {
return this.$store.state.adminList
}
},
watch: {
adminList () {
}
},
methods: {
updateShopId () {
this.$emit('input', this.form.shop_id) // input
}
}
}
</script>

View File

@ -0,0 +1,89 @@
<template>
<el-row type="flex" justify="space-around">
<el-col :span="8" class="fb f-s-14 fc">
{{ online ? showState : '离线' }}
</el-col>
<el-col :class="online ? heartAnimation ? 'icon-heart online' : 'icon-heart1 online' : 'icon-xinsui offline'"
class="iconfont f-s-26 fc" :span="8"></el-col>
<el-col :span="8" class="fb f-s-14 fc">
{{ getPlaneMode }}
</el-col>
</el-row>
</template>
<script>
export default {
name: 'HeartTag',
data () {
return {
heartAnimation: false, //
online: false,
isOnlineSetTimeout: null
}
},
props: {
heartBeat: {
deep: true
},
heartRandom: {
default: null,
deep: true
},
getPlaneMode: {
deep: true
}
},
computed: {
//
showState () {
let str = ''
if (Number(this.heartBeat) & 128) {
str += '已解'
} else {
str += '已锁'
}
return str
}
},
watch: {
heartRandom: {
handler (val) {
//
this.heartAnimation = true
setTimeout(() => {
this.heartAnimation = false
}, 500)
// 线
if (this.isOnlineSetTimeout) { // 线
clearInterval(this.isOnlineSetTimeout)
}
this.online = true
this.isOnlineSetTimeout = setTimeout(() => { // 10 线
this.online = false
}, 10000)
}
}
}
}
</script>
<style lang="scss" scoped>
@import "@/styles/theme.scss";
.el-row {
border-radius: 5px;
height: 36px;
line-height: 36px;
background-color: rgba($color: $white-color, $alpha: 0.5);
overflow: hidden;
}
.online {
background-color: rgba($color: $success-color, $alpha: 0.5);
}
.offline {
background-color: rgba($color: $danger-color, $alpha: 0.5);
}
</style>

View File

@ -0,0 +1,73 @@
<template>
<el-row class="p-r-10">
<el-col :class="state === 'danger' ? `${icon} dangerBG` : `${icon} normalBG`" class="iconfont f-s-22 fc "
:span="9"></el-col>
<el-col :span="unit !== '' ? 9 : 15" class="fb f-s-14 fr">
{{ val }}
</el-col>
<el-col v-if="unit !== ''" :span="6" class="fb f-s-14 fr">
{{ unit }}
</el-col>
</el-row>
</template>
<script>
export default {
name: 'PublicTag',
data () {
return {
}
},
props: {
icon: {
type: String,
default: '',
required: true,
deep: true
},
val: {
default: '',
required: true,
deep: true
},
unit: {
type: String,
default: '',
required: true,
deep: true
},
state: {
type: String,
default: 'normal',
required: true,
deep: true
}
}
}
</script>
<style lang="scss" scoped>
@import "@/styles/theme.scss";
.el-row {
border-radius: 5px;
height: 36px;
line-height: 36px;
background-color: rgba($color: $graylight-color, $alpha: 0.5);
max-width: 185px;
overflow: hidden;
.el-col {
overflow: hidden;
}
.normalBG {
background-color: rgba($color: $success-color, $alpha: 0.5);
}
.dangerBG {
background-color: rgba($color: $danger-color, $alpha: 0.5);
}
}
</style>

25
src/main.js Normal file
View File

@ -0,0 +1,25 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import '@/permission' // 路由守卫
import 'normalize.css/normalize.css'// A modern alternative to CSS resets
import '@/styles/index.scss'// 全局 css
import '@/styles/myIcon.scss'// 阿里巴巴图标
import 'mapbox-gl/dist/mapbox-gl.css'
// ElementUI
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import 'element-ui/lib/theme-chalk/base.css'// fade/zoom 等
import locale from 'element-ui/lib/locale/lang/zh-CN' // 引入中文语言包
Vue.use(ElementUI, { locale })
// 设置vue创建时 提示信息 false不提示
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')

56
src/permission.js Normal file
View File

@ -0,0 +1,56 @@
import router from './router'
import store from './store'
import NProgress from 'nprogress' // 进度条
import 'nprogress/nprogress.css' // 进度条 style
NProgress.configure({ showSpinner: false }) // 进度条 Configuration
/**
* @description: 路由守卫
*/
router.beforeEach((to, from, next) => {
// start 进度条
NProgress.start()
// 设置title
document.title = getPageTitle(to.meta.title)
// 路由
store.commit('user/initUser')// 用户信息初始化 PS:token不存在 执行且执行一次
const token = store.state.user.token
if (token === null) {
if (to.path === '/login') {
next()
NProgress.done()
} else {
next('/login')
NProgress.done()
}
} else {
if (to.path === '/login') {
next('/')
NProgress.done()
} else {
next()
NProgress.done()
}
}
})
/**
* @description: 路由完成 结束进度条
*/
router.afterEach(() => {
NProgress.done() // 结束进度条
})
/**
* @description: mateTile 全局title 组合
* @return: title
* @param {*} pageTitle 路由mate.tiele
*/
function getPageTitle (pageTitle) {
if (pageTitle) {
return `${pageTitle} - ${store.state.settings.title}`
} else {
return store.state.settings.title
}
}

167
src/router/index.js Normal file
View File

@ -0,0 +1,167 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/login'
import Page404 from '@/views/404'
import Layout from '@/views/layout/index'
Vue.use(VueRouter)
const routes = [
{
path: '/login',
name: 'Login',
component: Login,
meta: { title: '登录' },
roles: ['admin', 'editor'],
hidden: true
},
{
path: '/404',
name: 'Page404',
component: Page404,
meta: { title: '404' },
roles: ['admin', 'editor'],
hidden: true
},
{
path: '/',
redirect: '/home/index',
hidden: true
},
{
path: '/home',
component: Layout,
meta: { title: '概况', icon: 'iconfont icon-fuwudiqiu' },
redirect: '/home/index',
roles: ['admin', 'editor'],
children: [
{
path: '/home/index',
name: 'Home',
component: () => import('@/views/layout/components/main/home/index'),
meta: { title: '全图概况', icon: 'iconfont icon-fuwudiqiu' },
roles: ['admin', 'editor']
}
]
},
{
path: '/register',
component: Layout,
redirect: '/register/index',
meta: { title: '飞机管理', icon: 'el-icon-edit-outline' },
roles: ['admin', 'editor'],
children: [
{
path: '/register/index',
component: () => import('@/views/layout/components/main/register/index'),
meta: { title: '飞机列表', icon: 'el-icon-tickets' },
roles: ['admin', 'editor']
},
{
path: '/register/add/',
component: () => import('@/views/layout/components/main/register/add'),
meta: { title: '添加飞机', icon: 'el-icon-plus' },
roles: ['admin', 'editor']
},
{
path: '/register/edit/:id',
component: () => import('@/views/layout/components/main/register/add'),
meta: { title: '更新飞机', icon: 'el-icon-edit' },
roles: ['admin', 'editor'],
hidden: true
},
{
path: '/register/crosFrequency',
component: () => import('@/views/layout/components/main/register/crosFrequency'),
meta: { title: '飞机对频', icon: 'el-icon-link' },
roles: ['admin', 'editor']
}
]
},
{
path: '/route',
component: Layout,
redirect: '/route/index',
meta: { title: '航线管理', icon: 'iconfont icon-feihangluxian' },
roles: ['admin', 'editor'],
children: [
{
path: '/route/index',
component: () => import('@/views/layout/components/main/route/index'),
meta: { title: '航线列表', icon: 'iconfont icon-a-05-1-1jihuazhihanggenzong' },
roles: ['admin', 'editor']
},
{
path: '/route/add/',
component: () => import('@/views/layout/components/main/route/add'),
meta: { title: '设计航线', icon: 'iconfont icon-huizhi' },
roles: ['admin', 'editor']
},
{
path: '/route/edit/:id',
component: () => import('@/views/layout/components/main/route/add'),
meta: { title: '编辑航线', icon: 'iconfont icon-huizhi' },
roles: ['admin', 'editor'],
hidden: true
}
]
},
{
path: '/site',
component: Layout,
redirect: '/site/index',
meta: { title: '站点管理', icon: 'iconfont icon-zhandianguanli' },
roles: ['admin', 'editor'],
children: [
{
path: '/site/index',
component: () => import('@/views/layout/components/main/site/index'),
meta: { title: '站点列表', icon: 'el-icon-tickets' },
roles: ['admin', 'editor']
},
{
path: '/site/add/',
component: () => import('@/views/layout/components/main/site/add'),
meta: { title: '添加站点', icon: 'el-icon-plus' },
roles: ['admin', 'editor']
},
{
path: '/site/edit/:id',
component: () => import('@/views/layout/components/main/site/add'),
meta: { title: '更新站点', icon: 'el-icon-edit' },
roles: ['admin', 'editor'],
hidden: true
}
]
},
{
path: '/planes',
component: Layout,
redirect: '/planes/index',
meta: { title: '无人机', icon: 'iconfont icon-wurenji' },
roles: ['admin', 'editor'],
children: [
{
path: '/planes/index/:id/:name', // 动态加载路由时加ID参数
name: 'Planes',
component: () => import('@/views/layout/components/main/planes/index'),
meta: { title: '飞机加载中', icon: 'el-icon-loading' },
roles: ['admin', 'editor']
}
]
},
{
path: '*',
redirect: '/404',
roles: ['admin', 'editor'],
hidden: true
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router

30
src/settings.js Normal file
View File

@ -0,0 +1,30 @@
module.exports = {
/**
* @description: 全局title
*/
title: '送餐系统',
/**
* @description: api服务器
* host 主站
* api接口地址
* api常规接口路径
* api登录接口路径
*/
host: 'https://szdot.top',
baseURL: 'https://szdot.top/flycube.php',
apiPath: '/mpApi/Index/',
apiLoginPath: '/mpApi/Login/',
/**
* @description: mqtt服务器
* mqttHost 地址
* mqttPort 端口
* mqttUserName 用户名
* mqttPassword 密码
*/
mqttHost: 'wss://szdot.top',
mqttPort: 8083,
mqttUserName: 'admin',
mqttPassword: '123456'
}

410
src/store/index.js Normal file
View File

@ -0,0 +1,410 @@
import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
import settings from './modules/settings'
import user from './modules/user'
import api from '@/utils/api'
import { Message, MessageBox } from 'element-ui'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
adminList: [], // 管理员列表
airList: [], // 所有飞机列表
siteList: [], // 站点列表
routeList: [], // 航线列表
questList: [], // 订单列表
logList: [], // 操作日志列表
crosFrequency: null // 对频macadd
},
mutations: {
/**
* @description: 设置管理员列表
*/
setAdminList (state, list) {
state.adminList = list
},
/**
* @description: 设置飞机列表
*/
setAirList (state, list) {
state.airList = list
},
/**
* @description: 设置站点列表
*/
setSiteList (state, list) {
state.siteList = list
},
/**
* @description: 设置航线列表
*/
setRouteList (state, list) {
state.routeList = list
},
/**
* @description: 设置订单列表
*/
setQuestList (state, list) {
state.questList = list
},
/**
* @description: 向操作日志列表最后插入新的信息
* @param {*} logData
*/
insertNewLog (state, logData) {
state.logList.push(logData)
},
/**
* @description: 登记对频信息
*/
setCrosFrequency (state, macAdd) {
state.crosFrequency = macAdd
}
},
actions: {
/**
* @description: 获取管理员列表
*/
async fetchAdminList ({ commit }) {
const res = await api.get('getAdminList')
if (res.data.status === 1) {
commit('setAdminList', res.data.adminList)
} else {
Message.error(res.data.msg)
}
},
/**
* @description: 获取飞机列表
*/
async fetchAirList ({ commit, state }) {
const res = await api.get('getAirList')
res.data.airList.forEach(plane => {
plane.planeState = { // 飞机状态初始化字段
heartBeat: null, // 心跳
heartRandom: null, // 每次接收到心跳创建一个随机数 用于watch监听
voltagBattery: null, // 电压信息
currentBattery: null, // 电流信息
batteryRemaining: null, // 电池电量
positionAlt: null, // 高度信息
groundSpeed: null, // 对地速度
satCount: null, // 卫星数量
latitude: null, // 纬度
longitude: null, // 经度
fixType: null, // 定位状态
state: 1, // 飞机状态 默认初始状态为1
pingNet: null, // 网速测试
getPlaneMode: null, // 飞机模式
loadweight: null, // 重量
hookstatus: null, // 钩子状态
position: []// [[经度,维度,海拔高度]]累计数组
}
})
if (res.data.status === 1) {
commit('setAirList', res.data.airList)
} else {
commit('setSiteList', [])
Message.warning(res.data.msg)
}
return res.data.airList
},
/**
* @description: 创建新飞机
* @param {*} form 表单.飞机信息
* @return {*} 服务器返回信息
*/
async fetchAddAir ({ dispatch }, form) {
const params = new URLSearchParams()
params.append('shop_id', form.shop_id)
params.append('name', form.name)
params.append('date', form.date)
if (form.state) {
params.append('state', '1')
} else {
params.append('state', '0')
}
params.append('desc', form.desc)
const res = await api.post('addAir', params)
if (res.data.status === 1) {
await dispatch('fetchAirList')// 刷新飞机列表
Message.success(res.data.msg)
} else {
Message.error(res.data.msg)
}
return res
},
/**
* @description: 更新新飞机数据
* @param {*} form 表单.飞机信息
* @return {*} 服务器返回信息
*/
async fetchSaveAir ({ dispatch }, form) {
const params = new URLSearchParams()
params.append('shop_id', form.shop_id)
params.append('name', form.name)
params.append('date', form.date)
if (form.state) {
params.append('state', '1')
} else {
params.append('state', '0')
}
params.append('desc', form.desc)
params.append('id', form.id)
const res = await api.post('saveAir', params)
if (res.data.status === 1) {
await dispatch('fetchAirList')// 刷新飞机列表
Message.success(res.data.msg)
} else {
Message.error(res.data.msg)
}
return res
},
/**
* @description: 删除指定序号飞机
* @param {*} idArray 飞机序号数组
*/
fetchDelAir ({ dispatch }, idArr) {
if (idArr.length === 0) {
Message.error('未勾选记录')
} else {
MessageBox.confirm('请谨慎操作 确认删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const params = new URLSearchParams()
params.append('idArr', idArr)
api.post('deleteAir', params).then(res => {
if (res.data.status === 1) {
Message.success(res.data.msg)
dispatch('fetchAirList')// 刷新飞机列表
} else {
Message.error(res.data.msg)
}
})
}).catch(() => {
Message.info('取消操作')
})
}
},
/**
* @description: 获取站点列表
*/
async fetchSiteList ({ commit }) {
const res = await api.get('getSiteList')
if (res.data.status === 1) {
commit('setSiteList', res.data.siteList)
} else {
commit('setSiteList', [])
Message.warning(res.data.msg)
}
},
/**
* @description: 锁定站点
* @param {*} form 表单.站点信息
* @return {*} 服务器返回信息
*/
async fetchLockSite ({ dispatch }, form) {
const params = new URLSearchParams()
params.append('id', form.id)
params.append('runing', form.runing)
const res = await api.post('lockSite', params)
if (res.data.status === 1) {
await dispatch('fetchSiteList')// 刷新站点列表
Message.success(res.data.msg)
} else {
Message.error(res.data.msg)
}
return res
},
/**
* @description: 创建新站点
* @param {*} form 表单.站点信息
* @return {*} 服务器返回信息
*/
async fetchAddSite ({ dispatch }, form) {
const params = new URLSearchParams()
params.append('shop_id', form.shop_id)
params.append('sitename', form.sitename)
params.append('desc', form.desc)
params.append('upFile', form.upFile)
params.append('size', form.size)
params.append('bindroute', form.bindroute)
const res = await api.post('addSite', params)
if (res.data.status === 1) {
await dispatch('fetchSiteList')// 刷新站点列表
Message.success(res.data.msg)
} else {
Message.error(res.data.msg)
}
return res
},
/**
* @description: 创建新站点
* @param {*} form 表单.站点信息
* @return {*} 服务器返回信息
*/
async fetchSaveSite ({ dispatch }, form) {
const params = new URLSearchParams()
params.append('id', form.id)
params.append('shop_id', form.shop_id)
params.append('sitename', form.sitename)
params.append('desc', form.desc)
params.append('upFile', form.upFile)
params.append('size', form.size)
params.append('bindroute', form.bindroute)
const res = await api.post('saveSite', params)
if (res.data.status === 1) {
await dispatch('fetchSiteList')// 刷新站点列表
Message.success(res.data.msg)
} else {
Message.error(res.data.msg)
}
return res
},
/**
* @description: 删除指定序号站点
* @param {*} idArray 站点序号数组
*/
fetchDelSite ({ dispatch }, idArr) {
if (idArr.length === 0) {
Message.error('未勾选记录')
} else {
MessageBox.confirm('请谨慎操作 确认删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const params = new URLSearchParams()
params.append('idArr', idArr)
api.post('deleteSite', params).then(res => {
if (res.data.status === 1) {
Message.success(res.data.msg)
dispatch('fetchSiteList')// 刷新站点列表
} else {
Message.error(res.data.msg)
}
})
}).catch(() => {
Message.info('取消操作')
})
}
},
/**
* @description: 获取航线列表
*/
async fetchRouteList ({ commit }) {
const res = await api.get('getRouteList')
if (res.data.status === 1) {
commit('setRouteList', res.data.routeList)
} else {
Message.warning(res.data.msg)
}
return res.data.routeList
},
/**
* @description: 创建新航线
* @param {*} form 表单.航线信息
* @return {*} 服务器返回信息
*/
async fetchAddRoute ({ dispatch }, form) {
const params = new URLSearchParams()
params.append('shop_id', form.shop_id)
params.append('name', form.name)
params.append('desc', form.desc)
params.append('upFile', form.upFile)
const res = await api.post('addRoute', params)
if (res.data.status === 1) {
await dispatch('fetchRouteList')// 刷新站点列表
Message.success(res.data.msg)
} else {
Message.error(res.data.msg)
}
return res
},
/**
* @description: 更新航线
* @param {*} form 表单.航线信息
* @return {*} 服务器返回信息
*/
async fetchSaveRoute ({ dispatch }, form) {
const params = new URLSearchParams()
params.append('id', form.id)
params.append('shop_id', form.shop_id)
params.append('name', form.name)
params.append('desc', form.desc)
params.append('upFile', form.upFile)
const res = await api.post('saveRoute', params)
if (res.data.status === 1) {
await dispatch('fetchRouteList')// 刷新站点列表
Message.success(res.data.msg)
} else {
Message.error(res.data.msg)
}
return res
},
/**
* @description: 删除指定序号航线
* @param {*} idArray 航线序号数组
*/
fetchDelRoute ({ dispatch }, idArr) {
if (idArr.length === 0) {
Message.error('未勾选记录')
} else {
MessageBox.confirm('请谨慎操作 确认删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const params = new URLSearchParams()
params.append('idArr', idArr)
api.post('deleteRoute', params).then(res => {
if (res.data.status === 1) {
Message.success(res.data.msg)
dispatch('fetchRouteList')// 刷新航线列表
dispatch('fetchSiteList')// 刷新站点列表
} else {
Message.error(res.data.msg)
}
})
}).catch(() => {
Message.info('取消操作')
})
}
},
/**
* @description: 获取订单列表 ps:待发货及待收货 并且不是退款成功状态
* @return {*} 列表
*/
async fetchQuestList ({ commit }) {
const res = await api.get('getQuestList')
if (res.data.status === 1) {
commit('setQuestList', res.data.questList)
}
return res
},
/**
* @description: 向操作日志插入信息 并在积攒多条之后 向服务器提交
* @param {logData} content 日志内容 logData.color 时间
* @param {logData} color 标点颜色 默认#409EFF
* @param {logData} timestamp 插入时间 默认当前时间
*/
fetchLog ({ commit }, logData) {
/* 插入日志 */
logData.color = typeof logData.color !== 'undefined' ? logData.color : '#409EFF'
logData.timestamp = typeof logData.timestamp !== 'undefined' ? logData.color : new Date().getTime()
commit('insertNewLog', logData)
/* 积攒 向服务器提交 日志 待续写... */
}
},
modules: {
app,
settings,
user
},
getters: {
}
})
export default store

63
src/store/modules/app.js Normal file
View File

@ -0,0 +1,63 @@
const state = {
mqttState: false, // mqtt连接状态
isCollapse: localStorage.getItem('isCollapse') ? !!+localStorage.getItem('isCollapse') : true, // 侧边导航栏 显隐
isMobile: null, // 是否是pc端 true电脑端 false为移动端
defaultLngLat: null, // 地图默认经纬度
defaultZoom: null // 地图默认缩放
}
const mutations = {
// 导航栏 显隐
setCollapse () {
state.isCollapse = !state.isCollapse
if (state.isCollapse) {
localStorage.setItem('isCollapse', 1)
} else {
localStorage.setItem('isCollapse', 0)
}
},
// 判断是否pc端 还是移动端 true电脑端 false移动端
setIsMobile () {
const userAgentInfo = navigator.userAgent
const agents = ['Android', 'iPhone', 'SymbianOS', 'Windows Phone', 'iPad', 'iPod']
for (let i = 0; i < agents.length; i++) {
if (userAgentInfo.indexOf(agents[i]) > 0) {
state.isMobile = false
break
} else {
state.isMobile = true
}
}
},
// 设置地图默认经纬度
setDefaultLngLat (state, lngLat) {
state.defaultLngLat = lngLat
localStorage.setItem('defaultLngLat', JSON.stringify(lngLat))
},
// 设置地图默认缩放值
setDefaultZoom (state, zoom) {
state.defaultZoom = zoom
localStorage.setItem('defaultZoom', zoom)
}
}
const actions = {
}
const getters = {
// 获取地图默认经纬度 缓存中没有从 localStorage中获取 也没有设置为0
getDefaultLngLat () {
return state.defaultLngLat !== null ? state.defaultLngLat : localStorage.getItem('defaultLngLat') !== null ? JSON.parse(localStorage.getItem('defaultLngLat')) : { lng: 0, lat: 0 }
},
// 获取地图默认缩放值 缓存中没有从 localStorage中获取 也没有设置为1
getDefaultZoom () {
return state.defaultZoom !== null ? state.defaultZoom : localStorage.getItem('defaultZoom') !== null ? localStorage.getItem('defaultZoom') : 1
}
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}

View File

@ -0,0 +1,38 @@
import defaultSettings from '@/settings'
const {
title,
host,
baseURL,
apiPath,
apiLoginPath,
mqttHost,
mqttPort,
mqttUserName,
mqttPassword
} = defaultSettings
const state = {
title: title,
host: host,
baseURL: baseURL,
apiPath: apiPath,
apiLoginPath: apiLoginPath,
mqttHost: mqttHost,
mqttPort: mqttPort,
mqttUserName: mqttUserName,
mqttPassword: mqttPassword
}
const mutations = {
}
const actions = {
}
export default {
namespaced: true,
state,
mutations,
actions
}

42
src/store/modules/user.js Normal file
View File

@ -0,0 +1,42 @@
export default {
namespaced: true,
state: {
token: null,
name: null,
uname: null,
photo: null,
shop_id: null,
power: null
},
mutations: {
// 用户信息初始化
initUser (state) {
if (state.token == null) {
if (localStorage.getItem('token') != null) {
state.token = localStorage.getItem('token')
state.name = localStorage.getItem('name')
state.uname = localStorage.getItem('uname')
state.photo = localStorage.getItem('photo')
state.shop_id = localStorage.getItem('shop_id')
state.power = localStorage.getItem('power')
}
}
},
// 清除用户信息
destroyUser (state) {
state.token = null
state.name = null
state.uname = null
state.photo = null
state.shop_id = null
state.power = null
localStorage.removeItem('token')
}
},
actions: {
// 异步 清除用户信息
async destroyUserAsync (context) {
await context.commit('destroyUser')
}
}
}

View File

@ -0,0 +1,67 @@
// 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: 178px !important;
height: 178px !important;
}
// dropdown
.el-dropdown-menu {
a {
display: block
}
}
// to fix el-date-picker css style
.el-range-separator {
box-sizing: content-box;
}
.navbarBadge .is-fixed {
right: 30px !important;
top: 10px !important;
}
.el-drawer__header{
margin-bottom:15px;
}
.el-tabs__nav .is-top{
cursor: pointer;
}
.el-tabs__header{
margin-bottom: -16px;
}

302
src/styles/index.scss Normal file
View File

@ -0,0 +1,302 @@
@import "@/styles/theme.scss";
@import '@/styles/element-ui.scss';
@import '@/styles/transition.scss';
//通用
* {
margin: 0px;
padding: 0px;
}
html {
height: 100%;
}
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;
font-size: 14px;
color: $maintext-color;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
.router-link-active {
color: $maintext-color;
text-decoration: none !important;
}
a {
text-decoration: none !important;
}
a:focus,
a:active {
outline: none !important;
}
div:focus {
outline: none;
}
.clearfix {
&:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
}
label {
font-weight: 700;
}
.animation {
-webkit-transition: all 0.2s ease;
-moz-transition: all 0.2s ease;
-ms-transition: all 0.2s ease;
-o-transition: all 0.2s ease;
transition: all 0.2s ease;
}
// main-container global css
.app-container {
padding: 20px;
background-color: #fefefe;
height: 100%;
}
//mapboxgl
.mapboxgl-ctrl-bottom-left a {
display: none !important;
}
.mapboxgl-ctrl.mapboxgl-ctrl-attrib {
display: none !important;
}
//public
.borderLN {
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
}
.borderRN {
border-top-right-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
.l {
float: left !important;
}
.r {
float: right !important;
}
.clearB {
clear: both !important;
}
.border {
border: 1px solid #DCDFE6;
}
.mainFontColor {
color: #303133 !important;
}
.normalFontColor {
color: #606266 !important;
}
.secondaryFontColor {
color: #909399 !important;
}
.seatFontColor {
color: #C0C4CC !important;
}
.fc {
text-align: center;
}
.fr {
text-align: right;
}
.vm {
vertical-align: middle;
}
.bg-white {
background-color: $white-color;
}
.border-b-n {
border-bottom: none !important;
}
.border-t-n {
border-top: none !important;
}
.border-l-n {
border-left: none !important;
}
.border-r-n {
border-right: none !important;
}
.border-n {
border: none !important;
}
.fb {
font-weight: bold;
}
.el-row {
margin-top: 10px;
.el-header {
color: $maintext-color;
padding: 0px;
background-color: $border4-color;
}
.el-main {
padding: 0px;
}
}
//m-l-0 p-l-0 马根 帕丁
$position: (
"t":"top",
"b":"bottom",
"l":"left",
"r":"right",
);
$type: (
"p":"padding",
"m":"margin"
);
@each $item in $type {
@each $list in $position {
@for $i from 0 through 20 {
$val: $i * 1;
.#{nth($item,1)}-#{nth($list,1)}-#{$val} {
#{nth($item,2)}-#{nth($list,2)}: #{$val}px !important;
}
}
@for $i from 4 through 20 {
$val: $i * 5;
.#{nth($item,1)}-#{nth($list,1)}-#{$val} {
#{nth($item,2)}-#{nth($list,2)}: #{$val}px !important;
}
}
}
}
//m-0 p-0 马根 帕丁
$type: (
"p":"padding",
"m":"margin"
);
@each $item in $type {
@for $i from 0 through 100 {
$val: $i * 1;
.#{nth($item,1)}-#{$val} {
#{nth($item,2)}: #{$val}px !important;
}
}
}
//w-10 h-10 宽度百分比 高度百分比
$type: (
"w":"width",
"h":"height"
);
$mark: (
"%"
);
@each $item in $type {
@for $i from 0 through 10 {
$val: $i * 10;
.#{nth($item,1)}-#{$val} {
#{nth($item,2)}: #{$val}#{$mark};
}
}
}
//w-10px h-10px 宽度px 高度px 参数10 20 30...
$type: (
"w":"width",
"h":"height"
);
@each $item in $type {
@for $i from 1 through 80 {
$val: $i*10;
.#{nth($item,1)}-#{$val}px {
#{nth($item,2)}: #{$val}px;
}
}
}
//f-s-12 设置字体大小 1-50 60-200 50之前1递增 60以后10递增
$type: (
"f-s":"font-size",
);
@each $item in $type {
@for $val from 1 through 50 {
.#{nth($item,1)}-#{$val} {
#{nth($item,2)}: #{$val}px !important;
}
}
@for $i from 6 through 20 {
$val: $i*10;
.#{nth($item,1)}-#{$val} {
#{nth($item,2)}: #{$val}px !important;
}
}
}
//l-h-50 字体行高 12-100 1递增
$type: (
"l-h":"line-height",
);
@each $item in $type {
@for $val from 12 through 100 {
.#{nth($item,1)}-#{$val} {
#{nth($item,2)}: #{$val}px !important;
}
}
}

1
src/styles/myIcon.scss Normal file
View File

@ -0,0 +1 @@
@import 'https://at.alicdn.com/t/c/font_3703467_4teeodr1wrk.css'; //iconfont阿里巴巴

41
src/styles/theme.scss Normal file
View File

@ -0,0 +1,41 @@
//变量主题颜色
//主色
$brand-color: #409EFF;
//辅色
$success-color: #67C23A;
$warning-color: #E6A23C;
$danger-color: #F56C6C;
//字体颜色
$info-color: #909399;
$maintext-color: #303133;
$normaltext-color: #606266;
$secondarytext-color: #909399;
$holdtext-color: #C0C4CC;
//边框颜色
$border1-color: #DCDFE6;
$border2-color: #E4E7ED;
$border3-color: #EBEEF5;
$border4-color: #F2F6FC;
//基础
$black-color: #000000;
$white-color: #FFFFFF;
$graylight-color: #F2F6FC;
:export {
brand-color: $brand-color;
success-color: $success-color;
warning-color: $warning-color;
danger-color: $danger-color;
info-color: $info-color;
maintext-color: $maintext-color;
normaltext-color: $normaltext-color;
secondarytext-color: $secondarytext-color;
holdtext-color: $holdtext-color;
border1-color: $border1-color;
border2-color: $border2-color;
border3-color: $border3-color;
border4-color: $border4-color;
black-color: $black-color;
white-color: $white-color;
graylight-color: $graylight-color;
}

View File

@ -0,0 +1,48 @@
// global transition css
/* fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.28s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all .5s;
}
.fade-transform-enter {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* breadcrumb transition */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all .5s;
}
.breadcrumb-enter,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
.breadcrumb-move {
transition: all .5s;
}
.breadcrumb-leave-active {
position: absolute;
}

68
src/utils/api/index.js Normal file
View File

@ -0,0 +1,68 @@
import axios from 'axios'
import router from '@/router'
import store from '@/store'
// axios缺省设置
axios.defaults.baseURL = store.state.settings.baseURL
// axios.defaults.headers.post['Content-Type'] = 'application/json'
export default {
get,
post
}
/**
* @description: 异步 get方法 访问api
* @return: json.data对象
* @param {string} controller 控制器名称
* @param {bool} islogin 默认为index路径 否则访问login路径
*/
async function get (controller, islogin = false) {
const path = init(islogin)
try {
const res = await axios.get(path + controller)
if (res.data.status === -1) { // 权限过期
localStorage.removeItem('token')
router.go(0)
}
return res
} catch (error) {
console.error('Error fetching data:', error)
throw error
}
}
/**
* @description: 异步 post方法 访问api
* @return: json.data对象
* @param {string} controller 控制器名称
* @param {obj} params post参数
* @param {bool} islogin 默认为index路径 否则访问login路径
*/
async function post (controller, params, islogin = false) {
const path = init(islogin)
try {
const res = await axios.post(path + controller, params)
if (res.data.status === -1) { // 权限过期
localStorage.removeItem('token')
router.go(0)
}
return res
} catch (error) {
console.error('Error fetching data:', error)
throw error
}
}
/**
* @description: 判断访问常规还是登录接口路径 设置token头文件
* @return {*} 接口路径
* @param {bool} islogin
*/
function init (islogin) {
let path
if (islogin) {
axios.defaults.headers.common.Token = 'login'
path = store.state.settings.apiLoginPath
} else {
axios.defaults.headers.common.Token = store.state.user.token
path = store.state.settings.apiPath
}
return path
}

38
src/utils/api/table.js Normal file
View File

@ -0,0 +1,38 @@
import store from '@/store'
import api from '@/utils/api'
// import { Message, MessageBox } from 'element-ui'
import { Message } from 'element-ui'
/**
* @description: 向对频api 提交数据
*/
export async function apiCrosFrequency (params) {
const data = new URLSearchParams()// post对象参数 转成 字符串连接
data.append('macAdd', params.macAdd)
data.append('id', params.id)
const res = await api.post('crosFrequency', data)
if (res.data.status === 1) {
Message.success(res.data.msg)
store.dispatch('fetchAirList')// 更新飞机列表
} else {
Message.error(res.data.msg)
}
}
/**
* @description: 向改变订单承接任务api 提交数据 ()
* @param {*} id 订单id
* @param {*} state "quest"修改quest字段 "status"修改status字段
*/
export function questAss (id, state, val) {
const data = new URLSearchParams()// post对象参数 转成 字符串连接
data.append('id', id)
data.append('state', state)
data.append('val', val)
api.post('questAss', data).then(res => {
if (res.data.status === 1) {
Message.success(res.data.msg)
} else {
Message.error(res.data.msg)
}
})
}

24
src/utils/api/user.js Normal file
View File

@ -0,0 +1,24 @@
import api from '@/utils/api'
import router from '@/router'
import { Message } from 'element-ui'
export function login (params) {
const data = new URLSearchParams()// post对象参数 转成 字符串连接
data.append('username', params.username)
data.append('password', params.password)
api.post('login', data, true).then(
res => {
if (res.data.status === 1) {
localStorage.setItem('token', res.data.token)
localStorage.setItem('name', res.data.adminInfo.name)
localStorage.setItem('uname', res.data.adminInfo.uname)
localStorage.setItem('photo', res.data.adminInfo.photo)
localStorage.setItem('shop_id', res.data.adminInfo.shop_id)
localStorage.setItem('power', res.data.adminInfo.power)
router.replace('/')
} else {
Message.error(res.data.msg)
}
}
)
}

131
src/utils/index.js Normal file
View File

@ -0,0 +1,131 @@
/**
* Parse the time to string
* @param {(Object|string|number)} time
* @param {string} cFormat
* @returns {string | null}
*/
export function parseTime (time, cFormat) {
if (arguments.length === 0 || !time) {
return null
}
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
} else {
if ((typeof time === 'string')) {
if ((/^[0-9]+$/.test(time))) {
time = parseInt(time)
} else {
time = time.replace(new RegExp(/-/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()
}
// eslint-disable-next-line camelcase
const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
const value = formatObj[key]
// Note: getDay() returns 0 on Sunday
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
return value.toString().padStart(2, '0')
})
// eslint-disable-next-line camelcase
return time_str
}
/**
* @param {number} time
* @param {string} option
* @returns {string}
*/
export function formatTime (time, option) {
if (('' + time).length === 10) {
time = parseInt(time) * 1000
} else {
time = +time
}
const d = new Date(time)
const now = Date.now()
const diff = (now - d) / 1000
if (diff < 30) {
return '刚刚'
} else if (diff < 3600) {
// less 1 hour
return Math.ceil(diff / 60) + '分钟前'
} else if (diff < 3600 * 24) {
return Math.ceil(diff / 3600) + '小时前'
} else if (diff < 3600 * 24 * 2) {
return '1天前'
}
if (option) {
return parseTime(time, option)
} else {
return (
d.getMonth() +
1 +
'月' +
d.getDate() +
'日' +
d.getHours() +
'时' +
d.getMinutes() +
'分'
)
}
}
/**
* @param {string} url
* @returns {Object}
*/
export function param2Obj (url) {
const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
if (!search) {
return {}
}
const obj = {}
const searchArr = search.split('&')
searchArr.forEach(v => {
const index = v.indexOf('=')
if (index !== -1) {
const name = v.substring(0, index)
const val = v.substring(index + 1, v.length)
obj[name] = val
}
})
return obj
}
/**
* @param {string} path
* @returns {Boolean}
*/
export function isExternal (path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
/**
* @description: 统计当前被选中的ID组
* @return: 被选中记录 的ID组
* @param {*} selection 列表中被选中的记录
*/
export function countSelIdArr (selection) {
const idArr = []
selection.map((item) => idArr.push(item.id))
return idArr
}

89
src/utils/mqtt/index.js Normal file
View File

@ -0,0 +1,89 @@
import mqtt from 'mqtt'
import store from '@/store'
export default {
client: null, // mqtt连接 对象
url: store.state.settings.mqttHost, // mqtt服务器地址
options: { // mqtt服务器 参数配置
port: store.state.settings.mqttPort,
connectTimeout: 3000,
clientId: store.state.user.shop_id,
clean: true,
keepalive: 60,
username: store.state.settings.mqttUserName,
password: store.state.settings.mqttPassword
},
mqttConf,
mqttDestroy,
doSubscribe,
publishFun
}
/**
* @Description: mqtt对象初始化
*/
function mqttConf () {
if (!this.client) {
// 链接mqtt
this.client = mqtt.connect(this.url, this.options)
// mqtt服务器中断
this.client.on('error', () => {
store.dispatch('fetchLog', { content: '指令服务器异常中断' })
})
// 端口之后复连
this.client.on('reconnect', () => {
store.dispatch('fetchLog', { content: '重新连接指令服务器' })
})
// 监听断开连接事件
this.client.on('offline', () => {
store.dispatch('fetchLog', { content: '指令服务器异常中断' })
store.state.app.mqttState = false // 标记mqtt链接失败 掉线
})
}
}
/**
* @description: 获取主题消息
* @param {string} topic 主题名称
* @param {fun} callBack 回调 传一个对象{topic订阅的主题,msg接收到主题的数据} 在父级给获取
*/
function doSubscribe (topic, callBack) {
// 订阅主题
this.client.on('connect', () => {
if (store.state.app.mqttState === false) {
store.dispatch('fetchLog', { content: '成功连接指令服务器' })
}
store.state.app.mqttState = true // 标记mqtt链接成功 在线
// 订阅一个主题
this.client.subscribe(topic, { qos: 2 }, err => {
if (!err) {
store.dispatch('fetchLog', { content: topic + '主题订阅成功' })
} else {
store.dispatch('fetchLog', { content: topic + '主题订阅失败' })
}
})
})
// 获取订阅主题的消息
this.client.on('message', (topic, message) => {
callBack({ topic: topic, msg: message.toString() })
})
}
/**
* @description: 向指定topic发送消息
* @param {*} topic 发送信息到的主题 不包括前缀
* @param {*} msg 要发送的信息
*/
function publishFun (topic, msg) {
// topic要保持一致
this.client.publish(topic, msg, {
qos: 2
})
}
/**
* @Description: 销毁mqtt对象
*/
function mqttDestroy () {
if (this.client) {
this.client.end() // 离开页面的时候 关闭mqtt连接
this.client = null
}
}

254
src/views/404.vue Normal file
View File

@ -0,0 +1,254 @@
<template>
<div class="wscn-http404-container">
<div class="wscn-http404">
<div class="pic-404">
<img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
</div>
<div class="bullshit">
<div class="bullshit__oops">页面走丢了!</div>
<div class="bullshit__info">All rights reserved
<a style="color:#20a0ff" href="https://flicube.com" target="_blank">飞行魔方官网</a>
</div>
<div class="bullshit__headline">{{ message }}</div>
<div class="bullshit__info">Please check that the URL you entered is correct, or click the button below
to return to the homepage.</div>
<a href="/" class="bullshit__return-home">回首页</a>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Page404',
computed: {
message () {
return 'The webmaster said that you can not enter this page...'
}
}
}
</script>
<style lang="scss" scoped>
.wscn-http404-container {
transform: translate(-50%, -50%);
position: absolute;
top: 40%;
left: 50%;
}
.wscn-http404 {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
.pic-404 {
position: relative;
float: left;
width: 600px;
overflow: hidden;
&__parent {
width: 100%;
}
&__child {
position: absolute;
&.left {
width: 80px;
top: 17px;
left: 220px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
&.mid {
width: 46px;
top: 10px;
left: 420px;
opacity: 0;
animation-name: cloudMid;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1.2s;
}
&.right {
width: 62px;
top: 100px;
left: 500px;
opacity: 0;
animation-name: cloudRight;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px;
left: 420px;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px;
left: 500px;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
}
}
}
.bullshit {
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&__oops {
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__headline {
font-size: 20px;
line-height: 24px;
color: #222;
font-weight: bold;
opacity: 0;
margin-bottom: 10px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&__info {
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
margin-bottom: 30px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #ffffff;
opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
}
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<div class="fixed-bottom p-l-5" :class="maxWidth">
<el-button @click="handleOpenBlog" class="l p-3" type="text" size="mini" icon="iconfont icon-chuangkoufangda"
circle></el-button>
<div class="l p-l-10" v-if="newLog">
<span class="l m-t-5 m-r-5 logDot" :style="{ background: newLog.color }"></span>
<span>{{ newLog.timestamp | parseTime('{h}:{i}:{s}') }}</span>
<span class="m-l-10">{{ newLog.content }}</span>
</div>
<el-drawer title="程序日志" :modal-append-to-body="false" :visible.sync="drawer" direction="btt" size="50%">
<el-timeline :reverse="true" class="p-l-50 p-r-50">
<el-timeline-item v-for="(activity, index) in log" :key="index" :icon="activity.icon" :type="activity.type"
:color="activity.color" :size="activity.size"
:timestamp="activity.timestamp | parseTime('{y}-{m}-{d} {h}:{i}:{s}')">
{{ activity.content }}
</el-timeline-item>
</el-timeline>
</el-drawer>
</div>
</template>
<script>
import { parseTime } from '@/utils'
export default {
name: 'BlogBox',
data () {
return {
drawer: false
}
},
computed: {
maxWidth () {
return this.$store.state.app.isCollapse ? 'min' : 'max'
},
log () {
return this.$store.state.logList
},
newLog () {
return this.$store.state.logList[this.$store.state.logList.length - 1]
}
},
methods: {
handleOpenBlog () {
this.drawer = true
}
},
created () {
},
destroyed () {
},
filters: {
parseTime
}
}
</script>
<style scoped>
.fixed-bottom {
position: fixed;
bottom: 0;
right: 0;
z-index: 1002;
line-height: 24px;
height: 24px;
background: #F2F6FC;
-webkit-box-shadow: 0 0px 4px rgba(0, 21, 41, 0.3);
box-shadow: 0 0px 4px rgba(0, 21, 41, 0.3);
font-size: 14px;
}
.max {
width: calc(100% - 210px);
}
.min {
width: calc(100% - 60px);
}
.logDot {
width: 14px;
height: 14px;
border-radius: 10px;
overflow: hidden;
display: block;
}
</style>

View File

@ -0,0 +1,254 @@
<template>
<div>
<!-- 订单音效 -->
<audio controls="controls" hidden src="@/assets/sound/newMsg.mp3" ref="newMsg"></audio>
<audio controls="controls" hidden src="@/assets/sound/rebackMsg.mp3" ref="rebackMsg"></audio>
<!-- menu缩进按钮 -->
<div class="w-50px h-50px fc l" id="menuTabB" @click="handleCollapse">
<i class="iconfont f-s-26" :class="isCollapse ? 'icon-a-yousuojin3x' : 'icon-a-zuosuojin3x'"></i>
</div>
<!-- 面包屑 -->
<Breadcrumb class="l l-h-50 m-l-5" />
<!-- 头像 -->
<div class="right-menu m-r-5">
<el-dropdown class="avatar-container" trigger="click">
<div class="avatar-wrapper">
<img :src="avatar + '?imageView2/1/w/80/h/80'" class="user-avatar">
<i class="el-icon-caret-bottom" />
</div>
<el-dropdown-menu slot="dropdown" class="user-dropdown">
<el-dropdown-item divided>
<RouterLink to="/">
主控
</RouterLink>
</el-dropdown-item>
<el-dropdown-item divided @click.native="logout">
<span style="display:block;">退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<!-- 页面刷新 -->
<div class="l-h-50 p-r-15 r">
<el-button size="small" icon="iconfont icon-shuaxin" @click="refreshPage" circle></el-button>
</div>
<!-- 订单信息按钮 -->
<el-badge :hidden="orderCount == 0 ? true : false" :value="orderCount" class="navbarBadge l-h-50 p-r-15 r">
<el-button @click="drawer = true" size="small" :icon="orderIcon" circle></el-button>
</el-badge>
<el-drawer title="待处理订单" :visible.sync="drawer" size="40%" :append-to-body="true" :modal-append-to-body="false">
<el-tabs type="card">
<el-tab-pane label="未接订单">
<QuestTabs :fil='0' />
</el-tab-pane>
<el-tab-pane label="已接订单">
<QuestTabs :fil='1' />
</el-tab-pane>
<el-tab-pane label="申请退款订单">
<QuestTabs :fil='2' />
</el-tab-pane>
</el-tabs>
</el-drawer>
<!-- mqtt状态灯 -->
<div class="l-h-50 p-r-15 r">{{ mqttState === true ? "在线" : "掉线" }}</div>
</div>
</template>
<script>
import mqtt from '@/utils/mqtt'
import Breadcrumb from '@/components/Breadcrumb'
import QuestTabs from '@/components/QuestTabs'
export default {
name: 'Headbar',
data () {
return {
orderCount: null, //
rebackCount: null, // 退
drawer: false,
getQuestInterval: null, //
animationTrumpet: true //
}
},
components: {
Breadcrumb,
QuestTabs
},
computed: {
/**
* @description: 侧边栏显隐
*/
isCollapse () {
return this.$store.state.app.isCollapse
},
/**
* @description: mqtt服务器连接状态
*/
mqttState () {
return this.$store.state.app.mqttState
},
/**
* @description: 用户头像地址
*/
avatar () {
return this.$store.state.settings.host + this.$store.state.user.photo
},
/**
* @description: 获取订单列表 ps:待发货及待收货 并没有发起退款和正在退款状态
*/
questList () {
return this.$store.state.questList
},
/**
* @description: 面包条 订单图标动画
* @return {*} 图标样式
*/
orderIcon () {
if (this.orderCount === 0) {
return 'iconfont icon-meiyoudingdan-01'
} else {
if (this.animationTrumpet) {
return 'iconfont icon-tongzhi'
} else {
return 'iconfont icon-jinjidingdan'
}
}
}
},
methods: {
/**
* @description: 切换侧边栏 显隐
*/
handleCollapse () {
this.$store.commit('app/setCollapse')
},
/**
* @description: 计算订单数量 订单变化播放声音 显示在图标右上角小红圈内
* @param {*} list 要处理的订单列表
*/
computeQuestList (list) {
if (this.getQuestInterval !== null) {
clearInterval(this.getQuestInterval)
}
const waitShip = list.filter(item => item.status === '20')//
if (this.orderCount < waitShip.length && this.orderCount != null) { //
this.playMusic('newMsg')//
}
this.orderCount = waitShip.length//
const reBack = list.filter(item => item.back === '1')// 退
if (this.rebackCount < reBack.length && this.rebackCount != null) { //
this.playMusic('rebackMsg')// 退
}
this.rebackCount = reBack.length//
this.getQuestInterval = setInterval(() => {
this.animationTrumpet = !this.animationTrumpet//
}, 500)
},
//
playMusic (domId) {
const audioElement = this.$refs[domId]
if (!audioElement) {
console.error(`音频元素 ${domId} 不存在`)
return
}
audioElement.currentTime = 0 //
audioElement.play()
.then(() => {
console.log('音频播放成功')
})
.catch(error => {
console.error('音频播放失败', error)
})
setTimeout(() => {
audioElement.pause() //
}, 11000)
},
/**
* @description: 刷新当前页面
*/
refreshPage () {
mqtt.mqttDestroy()// mqtt
this.$router.go(0)//
},
/**
* @description: 登出
*/
logout () {
this.$store.dispatch('user/destroyUserAsync').then(
this.$router.push('/login')
)
}
},
watch: {
questList (val) {
this.computeQuestList(val)
}
}
}
</script>
<style lang="scss" scoped>
@import "@/styles/theme.scss";
#menuTabB:hover {
background-color: $border4-color;
cursor: pointer;
}
.el-dropdown-menu {
span {
color: $maintext-color;
}
}
.right-menu {
float: right;
height: 100%;
line-height: 50px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background .3s;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
}
.avatar-container {
margin-right: 30px;
.avatar-wrapper {
margin-top: 5px;
position: relative;
.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 10px;
}
.el-icon-caret-bottom {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
</style>

View File

@ -0,0 +1,83 @@
<template>
<div class="h-100">
<map-box ref="mapbox" :key="mapBoxKey" />
</div>
</template>
<script>
import MapBox from '@/components/MapBox'
export default {
name: 'Home',
data () {
return {
mapBoxKey: 0 // map-box
}
},
components: {
MapBox
},
computed: {
airList () {
return this.$store.state.airList
},
/**
* @description: 侧边栏显隐
*/
isCollapse () {
return this.$store.state.app.isCollapse
}
},
methods: {
/**
* @description: 创建飞机图标
*/
makePlanes (planes) {
this.$refs.mapbox.removePlanes()//
planes.forEach((plane, index) => { //
let planeDefaultLngLat
if (localStorage.getItem(plane.name) !== null) { //
planeDefaultLngLat = JSON.parse(localStorage.getItem(plane.name))
plane.lng = planeDefaultLngLat.lng
plane.lat = planeDefaultLngLat.lat
} else {
plane.lng = 0
plane.lat = 0
}
this.$refs.mapbox.makePlane(plane, index)
})
}
},
mounted () {
if (this.airList.length > 0) {
this.makePlanes(this.airList)//
}
},
watch: {
/**
* @description: 飞机列表更新时候 更新地图
*/
airList: {
handler (val) {
this.makePlanes(val)
}
},
/**
* @description: 侧边栏缩进有变化时 地图重新自适应
*/
isCollapse: {
handler (val) {
if (val) {
setTimeout(() => {
this.mapBoxKey++ // mapBoxKeymap-box
}, 500)
}
}
}
},
destroyed () {
}
}
</script>
<style></style>

View File

@ -0,0 +1,142 @@
<template>
<div class="h-100">
<map-box ref="mapbox" :key="mapBoxKey">
<template #content>
<PlaneStatus />
<ControllerTabs :plane="plane" @mapXOffset="mapXOffset" @makeRoute="makeRoute" @clearRoute="clearRoute" />
</template>
</map-box>
</div>
</template>
<script>
import MapBox from '@/components/MapBox'
import ControllerTabs from '@/components/ControllerTabs'
import PlaneStatus from '@/components/PlaneStatus'
export default {
name: 'Planes',
data () {
return {
mapBoxKey: '', // map-box
planesId: this.$route.params.id,
localCount: 0//
}
},
components: {
MapBox,
ControllerTabs,
PlaneStatus
},
computed: {
plane () {
if (!this.$store.state.airList) {
return null
}
return this.$store.state.airList.find(plane => plane.id === this.planesId)
},
position () {
if (this.plane) {
if (this.plane.planeState.position.length > 0) {
return this.plane.planeState.position
} else {
return []
}
} else {
return []
}
},
/**
* @description: 侧边栏显隐
*/
isCollapse () {
return this.$store.state.app.isCollapse
}
},
methods: {
/**
* @description: 创建飞机图标
*/
makePlane (plane) {
let planeDefaultLngLat
if (localStorage.getItem(plane.name)) { //
planeDefaultLngLat = JSON.parse(localStorage.getItem(plane.name))
plane.lng = planeDefaultLngLat.lng
plane.lat = planeDefaultLngLat.lat
} else {
plane.lng = 0
plane.lat = 0
}
this.$refs.mapbox.removePlanes()//
this.$refs.mapbox.makePlane(plane)//
this.$refs.mapbox.goto({ lng: plane.lng, lat: plane.lat })//
},
/**
* @description: 创建航线
*/
makeRoute (routeData) {
this.$refs.mapbox.makeRoute(routeData)
},
/**
* @description: 清楚航线
*/
clearRoute () {
this.$refs.mapbox.clearRoute()
},
/**
* @description: 屏幕横移
* @param {*} val 正数向左移动 负数向右移动
*/
mapXOffset (val) {
this.$refs.mapbox.mapXOffset(val)
}
},
mounted () {
if (this.plane !== undefined) {
this.makePlane(this.plane)//
}
},
watch: {
/**
* @description: 有飞机数据之后 在地图上创建飞机
*/
plane: {
handler (val) {
this.makePlane(val)
}
},
/**
* @description: 更新飞机位置 并画出轨迹 跟随飞机
*/
position: {
handler (val) {
const len = val.length
if (len > 2) {
const lng = val[len - 1][0]
const lat = val[len - 1][1]
this.localCount++//
if (this.localCount % 100 === 1) {
localStorage.setItem(this.plane.name, `{ "lng": ${lng}, "lat": ${lat} }`)
}
this.$refs.mapbox.setPlaneLngLat({ lng: lng, lat: lat }, 0, val, false)//
}
},
deep: true
},
/**
* @description: 侧边栏缩进有变化时 地图重新自适应
*/
isCollapse: {
handler (val) {
if (val) {
setTimeout(() => {
this.mapBoxKey++ // mapBoxKeymap-box
}, 500)
}
}
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,156 @@
<template>
<div class="app-container">
<el-row class="m-t-0">
<el-col :span="24" class="p-r-5">
<el-container>
<el-header height="42px" class="l-h-42 p-l-10 p-r-10 border border-b-n">
<div class="l">
<i v-if="pageState === 'add'" class="iconfont el-icon-plus f-s-20"></i>
<i v-else class="iconfont el-icon-edit f-s-20"></i>
<font class="m-l-10 f-s-18 fb">{{ $route.meta.title }}</font>
</div>
</el-header>
<el-main class="border p-20">
<el-form ref="form" :model="form" label-width="120px">
<el-form-item label="所属客户">
<el-select v-model="form.shop_id" filterable placeholder="请选择,也可输入搜索">
<el-option v-for="item in adminList" :key="item.id" :label="item.uname" :value="item.shop_id">
<span style="float: left">{{ item.name }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.uname
}}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="飞机名称">
<el-input v-model="form.name" placeholder="起名可以是中文" />
</el-form-item>
<el-form-item label="购买日期">
<el-date-picker v-if="pageState == 'add' ? true : false" v-model="form.date" type="date"
placeholder="选择日期" format="yyyy 年 MM 月 dd 日" value-format="timestamp">
</el-date-picker>
<el-date-picker v-else v-model="form.date" type="date" placeholder="选择日期" format="yyyy 年 MM 月 dd 日"
value-format="timestamp" disabled>
</el-date-picker>
</el-form-item>
<el-form-item :label="form.onoff === true ? '启用' : '停用'">
<el-switch v-model="form.onoff" />
</el-form-item>
<el-form-item label="飞机描述">
<el-input v-model="form.desc" type="textarea" placeholder="非必填" />
</el-form-item>
<el-form-item v-if="pageState == 'add' ? true : false">
<el-button type="primary" icon="el-icon-plus" @click="addAir">创建</el-button>
<el-button @click="setForm({})" class="iconfont icon-qingchu">
<font class="m-l-5">重填</font>
</el-button>
</el-form-item>
<el-form-item v-else>
<el-button type="primary" icon="el-icon-edit" @click="saveAir">更新</el-button>
</el-form-item>
</el-form>
</el-main>
</el-container>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
name: 'RegisterAdd',
data () {
return {
form: {
shop_id: '',
name: '',
date: '',
onoff: true,
desc: ''
},
airId: this.$route.params.id, // get id
pageState: '', //
plane: null
}
},
computed: {
//
adminList () {
return this.$store.state.adminList
},
//
airList () {
return this.$store.state.airList
}
},
methods: {
//
setForm (data) {
if (data.desc == null) {
data.desc = ''
}
this.form.shop_id = data.shop_id
this.form.name = data.name
this.form.date = data.date
this.form.onoff = data.onoff
this.form.desc = data.desc
if (Object.keys(data).length === 0) {
this.$message.warning('清空表单')
}
},
// or
initPage () {
if (this.airId === undefined) {
this.pageState = 'add'
} else {
this.pageState = 'edit'
this.plane = this.airList.find((item) => item.id === this.airId)
if (this.plane) {
const data = {
shop_id: this.plane.shop_id,
name: this.plane.name,
date: this.plane.apply_time + '000',
onoff: this.plane.onoff === '1',
desc: this.plane.describe
}
this.setForm(data)
}
}
},
/**
* @description: 创建新飞机
*/
async addAir () {
const res = await this.$store.dispatch('fetchAddAir', this.form)
if (res.data.status === 1) {
this.$router.push('/register/index')
}
},
/**
* @description: 更新飞机
*/
async saveAir () {
this.form.id = this.airId
const res = await this.$store.dispatch('fetchSaveAir', this.form)
if (res.data.status === 1) {
this.$router.push('/register/index')
}
}
},
watch: {
airList () {
this.initPage()//
}
},
created () {
if (this.airList.length > 0) {
this.initPage()//
}
}
}
</script>
<style scoped>
.line {
text-align: center;
}
</style>

View File

@ -0,0 +1,112 @@
<template>
<div class="app-container">
<el-row class="m-t-0">
<el-col :span="24" class="p-r-5">
<el-container>
<el-header height="42px" class="l-h-42 p-l-10 p-r-10 border border-b-n">
<div class="l">
<i class="iconfont el-icon-link f-s-20"></i>
<font class="m-l-10 f-s-18 fb">飞机对频</font>
</div>
</el-header>
<el-main class="border p-20">
<el-form label-width="120px">
<el-form-item label="物理ID">
<el-tag :type="macAdd === null ? 'danger' : ''">{{ macAdd === null ? 'NoThing' : macAdd }}</el-tag>
</el-form-item>
<el-form-item label="客户分配">
<el-select v-model="shop_id" filterable placeholder="请选择" @blur="air_id = ''; disabled = true"
:disabled="macAdd === null ? true : false">
<el-option v-for="item in adminList" :key="item.id" :label="item.uname" :value="item.shop_id">
<span style="float: left">{{ item.name }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.uname
}}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="飞机列表">
<el-radio-group v-if="airList.length != 0" v-model="air_id" @change="disabled = false;">
<el-radio class="m-b-10 m-l-0" v-for="item in airList" :key="item.id" :label="item.id" border>
<span></span>{{ item.name }}
{{ item.macadd }}
</el-radio>
</el-radio-group>
<span v-else>No data</span>
</el-form-item>
<el-form-item>
<el-button icon="iconfont icon-jiekou m-r-5" :type="macAdd === null ? 'info' : 'primary'"
@click="onSubmit" :disabled="macAdd === null ? 'disabled' : false">对频
</el-button>
</el-form-item>
</el-form>
</el-main>
</el-container>
</el-col>
</el-row>
</div>
</template>
<script>
import { apiCrosFrequency } from '@/utils/api/table'
export default {
name: 'crosFrequency',
data () {
return {
shop_id: null,
air_id: null
}
},
computed: {
//
adminList () {
return this.$store.state.adminList
},
//
airList () {
return this.$store.state.airList.filter((item) => item.shop_id === this.shop_id)
},
//
macAdd () {
return this.$store.state.crosFrequency
}
},
methods: {
/**
* @Description: 检查飞机发布过来的 mac地址 是否已经存在
* @Return: bool
*/
isMacAdd () {
let b = false
this.$store.state.airList.map((item) => {
if (item.macadd === this.macAdd) {
b = true
}
})
return b
},
/**
* @description: 提交表单
*/
onSubmit () {
const params = { macAdd: this.macAdd, id: this.air_id }
if (this.isMacAdd()) {
this.$confirm('已经有同名mac地址,请谨慎操作?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
apiCrosFrequency(params)//
}).catch(() => {
this.$message.info('取消对频')
})
} else {
apiCrosFrequency(params)//
}
}
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,144 @@
<template>
<div class="app-container">
<!-- 组合按钮 -->
<el-button-group>
<el-button type="primary" icon="el-icon-plus" @click="$router.replace('/register/add')">添加</el-button>
<el-button type="danger" icon="el-icon-delete" @click="deleteAir(countSelIdArr($refs.myTable.selection))">删除
</el-button>
<el-button type="warning" icon="el-icon-edit" @click="toEditPage()">编辑</el-button>
</el-button-group>
<!-- 用户select选项 -->
<el-button-group class="m-l-20">
<Selection v-model="form.shop_id" />
</el-button-group>
<!-- 飞机表格 -->
<el-table class="m-t-20 w-100" ref="myTable"
:data="airListArr.slice((currentPage - 1) * pageSize, currentPage * pageSize)" border tooltip-effect="dark">
<el-table-column align="center" type="selection" width="40">
</el-table-column>
<el-table-column align="center" prop="id" label="id" width="50">
</el-table-column>
<el-table-column prop="name" label="名称" width="150" min-width="150">
</el-table-column>
<el-table-column label="macID" width="175" min-width="175">
<template slot-scope="scope">
<el-tag :type="scope.row.macadd !== '' ? 'success' : 'danger'">
<i style="" class="iconfont m-r-5 f-s-16 l-h-32"
:class="scope.row.macadd !== '' ? 'icon-jiekou' : 'icon-cuowu'"></i>
<font>{{ scope.row.macadd !== '' ? scope.row.macadd : '未对频' }}</font>
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" min-width="150" show-overflow-tooltip>
<template slot-scope="scope">
<el-tag :type="scope.row.onoff === '1' ? 'success' : 'danger'">
<i style="" class="iconfont m-r-5 f-s-16 l-h-32"
:class="scope.row.onoff === '1' ? 'icon-qiyong' : 'icon-ic_tingyong'"></i>
<font>{{ scope.row.onoff === '1' ? '启用' : '停用' }}</font>
</el-tag>
</template>
</el-table-column>
<el-table-column label="注册时间" width="100" min-width="80">
<template slot-scope="scope">
{{ scope.row.apply_time | parseTime('{y}-{m}-{d}') }}
</template>
</el-table-column>
<el-table-column prop="controler" label="操作" width="200" min-width="200">
<template slot-scope="scope">
<el-button-group>
<el-button type="primary" icon="el-icon-search"
@click="$router.replace(`/planes/index/${scope.row.id}/${scope.row.name}`)">查看</el-button>
<el-button type="danger" icon="el-icon-delete" @click="deleteAir([scope.row.id])">删除</el-button>
</el-button-group>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination class="m-t-20" layout="prev, pager, next" :current-page.sync="currentPage" :page-size="pageSize"
:total="airList.length">
</el-pagination>
</div>
</template>
<script>
import { parseTime, countSelIdArr } from '@/utils'
import Selection from '@/components/Selection'
export default {
name: 'Register',
data () {
return {
pageSize: 8, //
currentPage: 1, //
form: {
shop_id: ''
}
}
},
components: {
Selection
},
computed: {
//
airList () {
return this.$store.state.airList
},
/**
* @description: 过滤掉 不对应客户的 飞机列表
* @return: 飞机列表
*/
airListArr () {
if (this.form.shop_id !== '') {
return this.airList.filter((item) => item.shop_id === this.form.shop_id)
} else {
return this.airList
}
}
},
methods: {
countSelIdArr,
/**
* @description: 跳转到编辑页面
*/
toEditPage () {
const selId = this.countSelIdArr(this.$refs.myTable.selection)
switch (selId.length) {
case 0:
this.$message.error('请选择一条需要编辑的记录')
break
case 1:
this.$router.push('/register/edit/' + selId['0'])
break
default:
this.$message.error('只能选择一条记录')
}
},
/**
* @description: 删除飞机
*/
deleteAir (idArr) {
this.$store.dispatch('fetchDelAir', idArr)
}
},
watch: {
airList () {
}
},
created () {
},
filters: {
parseTime,
countSelIdArr
}
}
</script>
<style lang="scss" scoped>
@import "@/styles/theme.scss";
.el-tag {
i {
vertical-align: middle
}
}
</style>

View File

@ -0,0 +1,228 @@
<template>
<div class="h-100">
<map-box ref="mapbox">
<template #content>
<el-row class="w-40 m-t-20 m-l-20">
<el-col :span="24" class="p-r-5">
<el-container>
<el-header height="42px" class="l-h-42 p-l-10 p-r-10 border border-b-n">
<div class="l">
<i v-if="pageState === 'add'" class="iconfont el-icon-plus f-s-20"></i>
<i v-else class="iconfont el-icon-edit f-s-20"></i>
<font class="m-l-10 f-s-18 fb">{{ $route.meta.title }}</font>
</div>
</el-header>
<el-main class="border p-20 mainBox bg-white">
<el-form ref="form" :model="form" label-width="120px">
<el-form-item label="所属客户">
<el-select v-model="form.shop_id" filterable placeholder="请选择,也可输入搜索">
<el-option v-for="item in $store.state.adminList" :key="item.id" :label="item.uname"
:value="item.shop_id" :disabled="pageState == 'edit' ? true : false">
<span style="float: left">{{ item.name }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.uname
}}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="航线标题">
<el-input v-model="form.name" placeholder="航线标题" />
</el-form-item>
<el-form-item label="航线文件上传">
<el-upload class="upload-demo" drag name="file" :action="action" :headers="myheader"
:on-success="handleUpSuccess" :on-exceed="handleExceed" :on-error="handleUpErr"
:on-remove="handleRemove" :limit="1" :file-list="fileList" :before-upload="beforeAvatarUpload">
<i class="el-icon-upload"></i>
<div class="el-upload__text"><em>点击上传</em></div>
</el-upload>
</el-form-item>
<el-form-item label="航线描述">
<el-input v-model="form.desc" type="textarea" placeholder="非必填" />
</el-form-item>
<el-form-item v-if="pageState == 'add' ? true : false">
<el-button type="primary" icon="el-icon-plus" @click="addRoute">创建</el-button>
</el-form-item>
<el-form-item v-else>
<el-button type="primary" icon="el-icon-edit" @click="saveRoute">更新</el-button>
</el-form-item>
</el-form>
</el-main>
</el-container>
</el-col>
</el-row>
</template>
</map-box>
</div>
</template>
<script>
import MapBox from '@/components/MapBox'
export default {
name: 'RouteAdd',
data () {
return {
action: this.$store.state.settings.baseURL + this.$store.state.settings.apiPath + 'upTxtFile',
myheader: { token: this.$store.state.user.token },
form: {
shop_id: '',
name: '',
desc: '',
upFile: '',
route_data: ''
},
routeId: this.$route.params.id,
fileList: [],
pageState: ''//
}
},
components: {
MapBox
},
computed: {
/**
* @description: 获取管理员列表
*/
adminList () {
return this.$store.state.adminList
},
/**
* @description: 获取航线列表
*/
routeList () {
return this.$store.state.routeList
}
},
methods: {
/* 文件上传表单 */
handleExceed () {
this.$message.warning('需先删除之前上传文件')
},
handleRemove () {
this.form.upFile = ''
},
handleUpSuccess (res) {
this.$refs.mapbox.makeRoute(res.content)// 线
if (res.status === 0) {
this.fileList = []
this.$message.error(res.msg)
} else {
this.form.upFile = res.data
this.$message.success(res.msg)
}
},
handleUpErr () {
this.$message.error('接口访问失败')
},
beforeAvatarUpload (file) {
const isTxt = file.type === 'text/plain' || file.type === 'application/json'
if (!isTxt) {
this.$message.error('航点文件仅支持txt或json格式!')
}
return isTxt
},
/* ED 文件上传表单 */
//
setForm (data) {
if (data.desc == null) {
data.desc = ''
}
this.form.shop_id = data.shop_id
this.form.name = data.name
this.form.upFile = data.upFile
this.form.desc = data.desc
this.form.route_data = data.route_data
if (Object.keys(data).length === 0) {
this.$message.warning('清空表单')
}
},
// or
initPage () {
if (this.routeId === undefined) {
this.pageState = 'add'
} else {
this.pageState = 'edit'
this.route = this.routeList.find((item) => item.id === this.routeId)
if (this.route) {
const data = {
shop_id: this.route.shop_id,
name: this.route.name,
route_data: this.route.route_data,
upFile: ''
}
this.setForm(data)
setTimeout(() => {
this.$refs.mapbox.makeRoute(JSON.parse(this.route.route_data))// 线
}, 0)
}
}
},
/**
* @description: 创建新站点
*/
async addRoute () {
const res = await this.$store.dispatch('fetchAddRoute', this.form)
if (res.data.status === 1) {
this.$router.push('/route/index')
}
},
/**
* @description: 更新站点
*/
async saveRoute () {
this.form.id = this.routeId
const res = await this.$store.dispatch('fetchSaveRoute', this.form)
if (res.data.status === 1) {
this.$router.push('/route/index')
}
}
},
watch: {
routeList () {
this.initPage()//
}
},
created () {
if (this.routeList.length > 0) {
this.initPage()//
}
}
}
</script>
<style lang="scss" scoped>
.line {
text-align: center;
}
.el-header,
.el-main {
z-index: 1001;
}
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
</style>

View File

@ -0,0 +1,127 @@
<template>
<div class="app-container">
<!-- 组合按钮 -->
<el-button-group>
<el-button type="primary" icon="el-icon-plus" @click="$router.replace('/route/add')">添加</el-button>
<el-button type="danger" icon="el-icon-delete" @click="deleteRoute(countSelIdArr($refs.myTable.selection))">删除
</el-button>
<el-button type="warning" icon="el-icon-edit" @click="toEditPage()">编辑</el-button>
</el-button-group>
<!-- 用户select选项 -->
<el-button-group class="m-l-20">
<Selection v-model="form.shop_id" />
</el-button-group>
<!-- 航线表格 -->
<el-table class="m-t-20 w-100" ref="myTable"
:data="routeListArr.slice((currentPage - 1) * pageSize, currentPage * pageSize)" border tooltip-effect="dark">
<el-table-column align="center" type="selection" width="40">
</el-table-column>
<el-table-column align="center" prop="id" label="id" width="50">
</el-table-column>
<el-table-column prop="name" label="任务标题" width="150" min-width="150">
</el-table-column>
<el-table-column prop="route_data" label="航线" min-width="150" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="controler" label="操作" width="150" min-width="150">
<template slot-scope="scope">
<el-button-group>
<el-button type="danger" icon="el-icon-delete" @click="deleteRoute([scope.row.id])">删除</el-button>
</el-button-group>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination class="m-t-20" layout="prev, pager, next" :current-page.sync="currentPage" :page-size="pageSize"
:total="routeListArr.length">
</el-pagination>
</div>
</template>
<script>
import { countSelIdArr } from '@/utils'
import Selection from '@/components/Selection'
export default {
name: 'Route',
data () {
return {
pageSize: 8, //
currentPage: 1, //
form: {
shop_id: ''
}
}
},
components: {
Selection
},
computed: {
/**
* @description: 获取航线列表
*/
routeList () {
return this.$store.state.routeList
},
/**
* @description: 过滤掉 不对应客户的 航线列表
* @return: 航线列表
*/
routeListArr () {
if (this.form.shop_id !== '') {
return this.routeList.filter((item) => item.shop_id === this.form.shop_id)
} else {
return this.routeList
}
}
},
methods: {
countSelIdArr,
/**
* @description: 跳转到编辑页面
*/
toEditPage () {
const selId = this.countSelIdArr(this.$refs.myTable.selection)
switch (selId.length) {
case 0:
this.$message.error('请选择一条需要编辑的记录')
break
case 1:
this.$router.push('/route/edit/' + selId['0'])
break
default:
this.$message.error('只能选择一条记录')
}
},
/**
* @description: 删除航线
*/
deleteRoute (idArr) {
this.$store.dispatch('fetchDelRoute', idArr)
}
},
watch: {
routeList () {
}
},
created () {
},
filters: {
countSelIdArr
}
}
</script>
<style lang="scss" scoped>
@import "@/styles/theme.scss";
.el-tag {
i {
vertical-align: middle
}
}
.image-placeholder {
position: relative;
display: inline-block;
}
</style>

View File

@ -0,0 +1,260 @@
<template>
<div class="app-container">
<el-row class="m-t-0">
<el-col :span="24" class="p-r-5">
<el-container>
<el-header height="42px" class="l-h-42 p-l-10 p-r-10 border border-b-n">
<div class="l">
<i v-if="pageState === 'add'" class="iconfont el-icon-plus f-s-20"></i>
<i v-else class="iconfont el-icon-edit f-s-20"></i>
<font class="m-l-10 f-s-18 fb">{{ $route.meta.title }}</font>
{{ form.bindroute }}
</div>
</el-header>
<el-main class="border p-20">
<el-form ref="form" :model="form" label-width="120px">
<el-form-item label="所属客户">
<el-select v-model="form.shop_id" filterable placeholder="请选择,也可输入搜索">
<el-option v-for="item in $store.state.adminList" :key="item.id" :label="item.uname"
:value="item.shop_id" :disabled="pageState == 'edit' ? true : false">
<span style="float: left">{{ item.name }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.uname
}}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="站点名称">
<el-input v-model="form.sitename" placeholder="取餐站点的名字" />
</el-form-item>
<el-form-item label="二维码尺寸">
<el-slider v-model="form.size" :format-tooltip="formatTooltip"></el-slider>
</el-form-item>
<el-form-item label="中心LOGO替换">
<el-upload class="avatar-uploader" drag name="file" :action="action" :headers="myheader"
:show-file-list="false" :on-success="handleUpSuccess" :on-error="handleUpErr"
:before-upload="beforeAvatarUpload">
<img v-if="form.upFile != ''"
:src="$store.state.settings.host + '/Data/UploadFiles/temp/' + form.upFile" class="avatar" />
<img v-else-if="defaultQr != '' && form.upFile == ''"
:src="$store.state.settings.host + '/Data/UploadFiles/qr/' + defaultQr" class="avatar" />
<template v-else>
<i class="el-icon-plus f-s-30 m-t-70 seatFontColor"></i>
<div class="el-upload__text"><em>空为默认LOGO</em></div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="绑定航线">
<el-transfer v-model="form.bindroute" :data="routeData" :titles="['可绑定航线', '已绑定航线']"></el-transfer>
</el-form-item>
<el-form-item label="站点描述">
<el-input v-model="form.desc" type="textarea" placeholder="非必填" />
</el-form-item>
<el-form-item v-if="pageState == 'add' ? true : false">
<el-button type="primary" icon="el-icon-plus" @click="addSite">创建</el-button>
</el-form-item>
<el-form-item v-else>
<el-button type="primary" icon="el-icon-edit" @click="saveSite">更新</el-button>
</el-form-item>
</el-form>
</el-main>
</el-container>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
name: 'SiteAdd',
data () {
return {
action: this.$store.state.settings.baseURL + this.$store.state.settings.apiPath + 'upImgFile',
myheader: { token: this.$store.state.user.token },
form: {
shop_id: '',
sitename: '',
desc: '',
upFile: '',
size: 100,
bindroute: []
},
siteId: this.$route.params.id,
pageState: '', //
defaultQr: ''
}
},
computed: {
/**
* @description: 获取管理员列表
*/
adminList () {
return this.$store.state.adminList
},
/**
* @description: 获取站点列表
*/
siteList () {
return this.$store.state.siteList
},
/**
* @description: 获取航线列表
*/
routeList () {
return this.$store.state.routeList
},
/**
* @description: 根据用户过滤出 对应的航线列表信息 ps:妈的估计只有三天之内的我才能看懂
*/
routeData () {
// shop_id
const filteredRoutes = this.routeList.filter(item => item.shop_id === this.form.shop_id)
.map(item => ({ key: item.id, label: item.name }))
// siteList bind_route 线 push 线 push
const bindRouteArray = []
this.siteList.forEach(element => {
if (this.pageState === 'edit') {
if (element.id !== this.siteId && element.bind_route !== null) {
element.bind_route.split(',').forEach(ele => {
bindRouteArray.push(ele)
})
}
} else {
if (element.bind_route !== null) {
element.bind_route.split(',').forEach(ele => {
bindRouteArray.push(ele)
})
}
}
})
// 使 bindRouteArray filteredRoutes
const finalFilteredRoutes = filteredRoutes.filter((item) =>
!bindRouteArray.includes(item.key.toString())
)
return finalFilteredRoutes
}
},
methods: {
/**
* @description: 设置滑动条值
* @param {*}
*/
formatTooltip (val) {
return val * 10 + 280 + '像素'
},
/* 文件上传表单 */
handleUpSuccess (res) {
if (res.status === 0) {
this.$message.error(res.msg)
} else {
this.form.upFile = res.data
this.$message.success(res.msg)
}
},
beforeAvatarUpload (file) {
const isJPG = file.type === 'image/jpeg'
if (!isJPG) {
this.$message.error('上传图片只能是JPG格式!')
}
return isJPG
},
handleUpErr () {
this.$message.error('接口访问失败')
},
//
setForm (data) {
if (data.desc == null) {
data.desc = ''
}
this.form.shop_id = data.shop_id
this.form.sitename = data.sitename
this.form.desc = data.desc
this.form.bindroute = data.bindroute
this.defaultQr = data.defaultQr
if (Object.keys(data).length === 0) {
this.$message.warning('清空表单')
}
},
// or
initPage () {
if (this.siteId === undefined) {
this.pageState = 'add'
} else {
this.pageState = 'edit'
this.site = this.siteList.find((item) => item.id === this.siteId)
if (this.site) {
const data = {
shop_id: this.site.shop_id,
sitename: this.site.sitename,
desc: this.site.describe,
bindroute: this.site.bind_route !== null ? this.site.bind_route.split(',') : [], //
defaultQr: this.site.qr
}
this.setForm(data)
}
}
},
/**
* @description: 创建新站点
*/
async addSite () {
const res = await this.$store.dispatch('fetchAddSite', this.form)
if (res.data.status === 1) {
this.$router.push('/site/index')
}
},
/**
* @description: 更新站点
*/
async saveSite () {
this.form.id = this.siteId
const res = await this.$store.dispatch('fetchSaveSite', this.form)
if (res.data.status === 1) {
this.$router.push('/site/index')
}
}
},
watch: {
siteList () {
this.initPage()//
}
},
created () {
if (this.siteList.length > 0) {
this.initPage()//
}
}
}
</script>
<style lang="scss" scoped>
.line {
text-align: center;
}
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
</style>

View File

@ -0,0 +1,143 @@
<template>
<div class="app-container">
<!-- 组合按钮 -->
<el-button-group>
<el-button type="primary" icon="el-icon-plus" @click="$router.replace('/site/add')">添加</el-button>
<el-button type="danger" icon="el-icon-delete" @click="deleteSite(countSelIdArr($refs.myTable.selection))">删除
</el-button>
<el-button type="warning" icon="el-icon-edit" @click="toEditPage()">编辑</el-button>
</el-button-group>
<!-- 用户select选项 -->
<el-button-group class="m-l-20">
<Selection v-model="form.shop_id" />
</el-button-group>
<!-- 站点表格 -->
<el-table class="m-t-20 w-100" ref="myTable"
:data="siteListArr.slice((currentPage - 1) * pageSize, currentPage * pageSize)" border tooltip-effect="dark">
<el-table-column align="center" type="selection" width="40">
</el-table-column>
<el-table-column align="center" prop="id" label="id" width="50">
</el-table-column>
<el-table-column prop="sitename" label="站点名称" width="120" min-width="100">
</el-table-column>
<el-table-column label="菊花码缩率图" width="120" min-width="150">
<template slot-scope="scope">
<el-image :src="$store.state.settings.host + '/Data/UploadFiles/qr/' + scope.row.qr"
:preview-src-list="[$store.state.settings.host + '/Data/UploadFiles/qr/' + scope.row.qr]">
</el-image>
</template>
</el-table-column>
<el-table-column prop="describe" label="站点描述" min-width="80" show-overflow-tooltip>
</el-table-column>
<el-table-column align="center" label="已绑航线" width="200">
<template slot-scope="scope">
<el-tag class="iconfont" :class="scope.row.bind_route !== null ? 'icon-feihangluxian' : 'icon-ic_tingyong'"
:type="scope.row.bind_route !== null ? '' : 'danger'">
<font class="m-l-5">{{ scope.row.bind_route !== null ? scope.row.bind_route :
"未绑定" }}</font>
</el-tag>
</template>
</el-table-column>
<el-table-column prop="controler" label="操作" width="140" min-width="140">
<template slot-scope="scope">
<el-button-group>
<el-button type="danger" icon="el-icon-delete" @click="deleteSite([scope.row.id])">删除</el-button>
</el-button-group>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination class="m-t-20" layout="prev, pager, next" :current-page.sync="currentPage" :page-size="pageSize"
:total="siteListArr.length">
</el-pagination>
</div>
</template>
<script>
import { countSelIdArr } from '@/utils'
import Selection from '@/components/Selection'
export default {
name: 'Site',
data () {
return {
pageSize: 8, //
currentPage: 1, //
form: {
shop_id: ''
}
}
},
components: {
Selection
},
computed: {
/**
* @description: 获取站点列表
*/
siteList () {
return this.$store.state.siteList
},
/**
* @description: 过滤掉 不对应客户的 站点列表
* @return: 站点列表
*/
siteListArr () {
if (this.form.shop_id !== '') {
return this.siteList.filter((item) => item.shop_id === this.form.shop_id)
} else {
return this.siteList
}
}
},
methods: {
countSelIdArr,
/**
* @description: 跳转到编辑页面
*/
toEditPage () {
const selId = this.countSelIdArr(this.$refs.myTable.selection)
switch (selId.length) {
case 0:
this.$message.error('请选择一条需要编辑的记录')
break
case 1:
this.$router.push('/site/edit/' + selId['0'])
break
default:
this.$message.error('只能选择一条记录')
}
},
/**
* @description: 删除站点
*/
deleteSite (idArr) {
this.$store.dispatch('fetchDelSite', idArr)
}
},
watch: {
siteList () {
}
},
created () {
},
filters: {
countSelIdArr
}
}
</script>
<style lang="scss" scoped>
@import "@/styles/theme.scss";
.el-tag {
i {
vertical-align: middle
}
}
.image-placeholder {
position: relative;
display: inline-block;
}
</style>

View File

@ -0,0 +1,204 @@
<template>
<div>
<!-- logo -->
<div class="sidebar-logo-container" :class="{ 'collapse': isCollapse }">
<router-link v-if="isCollapse" key="isCollapse" class="sidebar-logo-link" to="/">
<img v-if="logo" src="@/assets/logo.png" class="sidebar-logo">
<h1 v-else class="sidebar-title">{{ title }}</h1>
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" src="@/assets/logo.png" class="sidebar-logo">
<h1 class="sidebar-title">{{ title }}</h1>
</router-link>
</div>
<!-- end logo -->
<!-- menu -->
<el-menu class="border-n" :router="true" :default-active="activeMenu" :unique-opened="true" background-color="#304156"
text-color="rgb(191, 203, 217)" active-text-color="#409EFF" :collapse-transition="false" :collapse="isCollapse">
<template v-for="(route, index) in routes">
<el-menu-item v-if="route.children.length < 2" :key="route.path" :index="route.children[0].path">
<i class="fc" :class="route.children[0].meta.icon"></i>
<span slot="title">{{ route.children[0].meta.title }}</span>
</el-menu-item>
<el-submenu v-else :key="index" :index="route.path">
<template slot="title">
<i class="fc" :class="route.meta.icon"></i>
<span>{{ route.meta.title }}</span>
</template>
<el-menu-item v-for="child in route.children" :key="child.path" :index="child.path">
<i class="fc" :class="child.meta.icon"></i>
<span>{{ child.meta.title }}</span>
</el-menu-item>
</el-submenu>
</template>
</el-menu>
<!-- end menu -->
</div>
</template>
<script>
export default {
name: 'Menubar',
data () {
return {
title: '无人机控制终端',
logo: '@/assets/logo.png'
}
},
computed: {
/**
* @description: 递归过滤路由列表同时过滤掉父级和子级中 hidden true 或权限不足的项
*/
routes () {
const filterRoutes = (routes, userPower) => {
return routes.filter(item => {
const roles = item.roles || [] //
if (item.hidden === true || roles.indexOf(userPower) === -1) {
return false
}
if (item.children && item.children.length > 0) {
item.children = filterRoutes(item.children, userPower) //
if (item.children.length === 0) {
return false //
}
}
return true
})
}
const userPower = this.$store.state.user.power.trim()
return filterRoutes(this.$router.options.routes, userPower)
},
/**
* @description: 当前激活的 路由路径
*/
activeMenu () {
const route = this.$route
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
/**
* @description: 获取飞机列表
*/
airList () {
return this.$store.state.airList.filter(element => element.shop_id === this.$store.state.user.shop_id)
},
/**
* @description: 侧边导航栏状态
*/
isCollapse () {
return this.$store.state.app.isCollapse
}
},
methods: {
/**
* @description: 动态加载路由
*/
loadRoute () {
const arr = new Array(0)
this.airList.map((item, index) => {
arr[index] = {
path: '/planes/index/' + item.id + '/' + item.name,
name: 'Planes',
component: () => import('@/views/layout/components/main/planes/index.vue'),
meta: { title: item.name, icon: 'iconfont icon-wurenji' },
roles: ['admin', 'editor']
}
})
this.routes.map((element) => {
if (element.meta.title === '无人机') {
element.children = arr
}
})
}
},
created () {
},
watch: {
/**
* @description: 监听飞机列表 有个新刷新导航栏
*/
airList () {
this.loadRoute()
this.$forceUpdate()//
}
}
}
</script>
<style lang="scss" scoped>
@import "@/styles/theme.scss";
i {
font-size: 18px !important;
margin-right: 12px;
vertical-align: middle;
}
.el-submenu__title i,
.el-menu-item i {
color: rgb(191, 203, 217);
}
.el-submenu {
.el-menu-item {
background-color: #1f2d3d !important;
}
}
.el-submenu .el-menu-item:hover {
background-color: #001528 !important;
}
.sidebarLogoFade-enter,
.sidebarLogoFade-leave-to {
opacity: 0;
}
h1 {
color: $maintext-color !important;
}
.sidebar-logo-container {
position: relative;
width: 100%;
height: 50px;
line-height: 50px;
background: $graylight-color;
text-align: center;
overflow: hidden;
z-index: 100;
box-shadow: 0 1px 1px rgba(0, 21, 41, .08);
& .sidebar-logo-link {
height: 100%;
width: 100%;
& .sidebar-logo {
width: 24px;
height: 24px;
vertical-align: middle;
margin-right: 12px;
}
& .sidebar-title {
display: inline-block;
margin: 0;
color: #fff;
font-weight: 600;
line-height: 50px;
font-size: 14px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
vertical-align: middle;
}
}
&.collapse {
.sidebar-logo {
margin-right: 0px;
}
}
}
</style>

162
src/views/layout/index.vue Normal file
View File

@ -0,0 +1,162 @@
<template>
<el-container id="layoutBox">
<el-aside class="animation" :class="isCollapse ? 'menuW' : 'menuS'">
<Menubar />
</el-aside>
<el-container>
<el-header>
<Headbar />
</el-header>
<el-main>
<router-view :key="rouKey" />
</el-main>
<el-footer>
<BlogBox />
</el-footer>
</el-container>
</el-container>
</template>
<script>
import mqtt from '@/utils/mqtt'
import Menubar from '@/views/layout/components/menubar'
import Headbar from '@/views/layout/components/headbar'
import BlogBox from '@/views/layout/components/BlogBox'
export default {
name: 'Layout',
data () {
return {
frequencySetTimeout: null //
}
},
methods: {
},
components: {
Menubar,
Headbar,
BlogBox
},
computed: {
rouKey () {
return this.$route.path
},
isCollapse () {
return this.$store.state.app.isCollapse
},
shop_id () {
return this.$store.state.user.shop_id
},
airList () {
return this.$store.state.airList
}
},
created () {
},
mounted () {
/* init */
this.$store.commit('app/setIsMobile')//
this.$store.dispatch('fetchAdminList')//
this.$store.dispatch('fetchSiteList')//
this.$store.dispatch('fetchRouteList')// 线
this.$store.dispatch('fetchQuestList')// ps: 退
this.$store.dispatch('fetchAirList')//
},
watch: {
/**
* @description: 异步拿到飞机列表之后 再进行一些初始化操作
* @param {*} res 飞机列表
*/
airList (res) {
/* mqtt */
mqtt.mqttConf()// mqtt
//
mqtt.doSubscribe('planeState/+', (mqttRes) => {
res.forEach(plane => {
if (mqttRes.topic.indexOf(plane.macadd) > -1) {
// mqtt
const jsonData = JSON.parse(mqttRes.msg.trim())
// mqtt
for (const key in jsonData) {
if (key === 'heartBeat') { // heartRandom watch
plane.planeState.heartRandom = Math.random()
}
if (key === 'position') { // 便
let position = jsonData.position
position = position.replace(/([a-zA-Z0-9]+):/g, '"$1":')// mcujson "lng":
position = JSON.parse(position)
plane.planeState.position.push([position.lng / 10e6, position.lat / 10e6, Number(position.alt)])
if (plane.planeState.position.length > 1000) {
plane.planeState.position.shift()//
}
} else {
plane.planeState[key] = jsonData[key]//
}
}
}
})
})
//
mqtt.doSubscribe('crosFrequency/+', (res) => {
if (res.topic.indexOf('crosFrequency') > -1) {
this.$store.commit('setCrosFrequency', res.msg)//
clearTimeout(this.frequencySetTimeout)//
this.frequencySetTimeout = setTimeout(() => { // 10
this.$store.commit('setCrosFrequency', null)
}, 10000)
}
})
//
mqtt.doSubscribe(`refreshQuestList/${this.shop_id}`, (res) => {
if (res.topic.indexOf(`refreshQuestList/${this.shop_id}`) > -1) {
this.$store.dispatch('fetchSiteList')//
this.$store.dispatch('fetchQuestList')// ps: 退
}
})
}
},
destroyed () {
mqtt.mqttDestroy()// mqtt
}
}
</script>
<style lang="scss" scoped>
@import "@/styles/theme.scss";
#layoutBox {
height: 100%;
}
.el-aside {
background-color: #304156;
overflow: hidden !important;
}
.menuW {
width: 60px !important;
}
.menuS {
width: 210px !important;
}
.el-header {
padding: 0 !important;
height: 50px !important;
line-height: 50px !important;
overflow: hidden;
z-index: 99;
box-shadow: 0 0px 4px rgba(0, 21, 41, .08);
}
.el-main {
padding: 0 !important;
height: 100%;
background-color: $white-color;
}
.el-footer {
height: 24px !important;
}
</style>

188
src/views/login.vue Normal file
View File

@ -0,0 +1,188 @@
<template>
<div class="login-container">
<div class="title-container f-s-32 fb m-t-20 m-l-20 l-h-32">
<img src="@/assets/logo.png" class="m-r-15">
<font class="f1 f-s-22">飞行魔方</font>
<font class="f2 f-s-10 m-l-5" style="vertical-align :top">v.1.0.1</font>
</div>
<el-form ref="loginForm" :model="loginForm" class="login-form" auto-complete="on" label-position="left">
<h3 class="f-s-30 m-b-15 fc">Authorize</h3>
<el-form-item prop="username">
<span class="svg-container"><i class="iconfont icon-yonghuziliao f-s-24"></i></span>
<el-input ref="username" v-model="loginForm.username" placeholder="用户" name="username" type="text" tabindex="1"
auto-complete="on" />
</el-form-item>
<el-form-item prop="password">
<span class="svg-container">
<i class="iconfont icon-suoding f-s-24"></i>
</span>
<el-input :key="passwordType" ref="password" v-model="loginForm.password" :type="passwordType" placeholder="密码"
name="password" tabindex="2" auto-complete="on" @keyup.enter.native="handleLogin" />
<span class="show-pwd" @click="showPwd">
<i class="f-s-24"
:class="passwordType === 'password' ? 'iconfont icon-biyanjing' : 'iconfont icon-yanjing'"></i>
</span>
</el-form-item>
<el-button icon="el-icon-user" :loading="loading" type="primary" style="width:100%;margin-bottom:30px;"
@click.native.prevent="handleLogin">登录</el-button>
</el-form>
</div>
</template>
<script>
import { login } from '@/utils/api/user'
export default {
name: 'Login',
data () {
return {
loginForm: {
username: '',
password: ''
},
loading: false,
passwordType: 'password'
}
},
methods: {
/**
* @description: 显示密码
*/
showPwd () {
if (this.passwordType === 'password') {
this.passwordType = ''
} else {
this.passwordType = 'password'
}
this.$nextTick(() => {
this.$refs.password.focus()
})
},
login,
/**
* @description:提交登陆
*/
handleLogin () {
login({
username: this.loginForm.username,
password: this.loginForm.password
})
}
},
created () {
}
}
</script>
<style lang="scss">
$bg: #283443;
$light_gray: #fff;
$cursor: #fff;
@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
.login-container .el-input input {
color: $cursor;
}
}
/* reset element-ui css */
.login-container {
.el-input {
display: inline-block;
height: 47px;
width: 85%;
input {
background: transparent;
border: 0px;
border-radius: 0px;
padding: 12px 5px 12px 15px;
color: $light_gray;
height: 47px;
caret-color: $cursor;
&:-webkit-autofill {
box-shadow: 0 0 0px 1000px $bg inset !important;
-webkit-text-fill-color: $cursor !important;
}
}
}
.el-form-item {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.1);
border-radius: 5px;
color: #454545;
}
}
</style>
<style lang="scss" scoped>
$bg: #2d3a4b;
$dark_gray: #889aa4;
$light_gray: #eee;
h3 {
color: $light_gray;
}
.login-container {
min-height: 100%;
width: 100%;
background-color: $bg;
overflow: hidden;
.login-form {
position: relative;
width: 520px;
max-width: 100%;
padding: 160px 35px 0;
margin: 0 auto;
overflow: hidden;
}
.tips {
font-size: 14px;
color: #fff;
margin-bottom: 10px;
span {
&:first-of-type {
margin-right: 16px;
}
}
}
.svg-container {
padding: 6px 5px 6px 15px;
color: $dark_gray;
vertical-align: middle;
width: 30px;
display: inline-block;
}
.title-container {
color: $light_gray;
img {
vertical-align: middle;
}
.f2 {
vertical-align: top;
color: $dark_gray;
}
}
.show-pwd {
position: absolute;
right: 10px;
top: 7px;
font-size: 16px;
color: $dark_gray;
cursor: pointer;
user-select: none;
}
}
</style>