Browse Source

后台管理系统

master
liupeng 6 months ago
parent
commit
a4630ced62
  1. 3
      package.json
  2. 146
      src/router/index.ts
  3. 74
      src/views/Header.vue
  4. 152
      src/views/HomeView.vue
  5. 85
      src/views/Sidebar.vue
  6. 58
      src/views/dashboard/EChartWrapper.vue
  7. 280
      src/views/dashboard/index.vue
  8. 12
      src/views/device/chargers.vue
  9. 12
      src/views/device/locations.vue
  10. 12
      src/views/device/points.vue
  11. 12
      src/views/device/vehicles.vue
  12. 12
      src/views/inventory/account.vue
  13. 12
      src/views/inventory/query.vue
  14. 12
      src/views/log/device.vue
  15. 12
      src/views/log/upstream.vue
  16. 12
      src/views/modelingSimulation/index.vue
  17. 12
      src/views/taskManagement/automatedPresentation.vue
  18. 12
      src/views/taskManagement/taskQuery.vue
  19. 12
      src/views/user/roles.vue
  20. 12
      src/views/user/users.vue

3
package.json

@ -13,7 +13,8 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@vueuse/core": "^13.2.0" "@vueuse/core": "^13.2.0",
"echarts": "^5.6.0"
}, },
"devDependencies": { "devDependencies": {
"@ease-forge/runtime": "^1.0.12", "@ease-forge/runtime": "^1.0.12",

146
src/router/index.ts

@ -7,7 +7,151 @@ const router = createRouter({
path: '/', path: '/',
name: 'home', name: 'home',
// 自动引导到 /editor // 自动引导到 /editor
redirect: '/editor' component: () => import('@/views/HomeView.vue'),
children: [
{
path: '/dashboard',
name: 'dashboard',
meta: {
title: '仪表盘'
},
component: () => import('@/views/dashboard/index.vue')
},
{
path: '/modelingSimulation',
name: 'modelingSimulation',
meta: {
title: '建模仿真控制平台'
},
component: () => import('@/views/modelingSimulation/index.vue')
},
{
path: '/taskManagement',
name: 'taskManagement',
children:[
{
path: '/taskQuery',
name: 'taskQuery',
meta: {
title: '任务查询'
},
component: () => import('@/views/taskManagement/taskQuery.vue')
},
{
path: '/automatedPresentation',
name: 'automatedPresentation',
meta: {
title: '自动演示管理'
},
component: () => import('@/views/taskManagement/automatedPresentation.vue')
}
]
},
{
path: '/log',
name: 'log',
children:[
{
path: '/upstream',
name: 'upstream',
meta: {
title: '上游接口日志'
},
component: () => import('@/views/log/upstream.vue')
},
{
path: '/device',
name: 'device',
meta: {
title: '设备报文日志'
},
component: () => import('@/views/log/device.vue')
}
]
},
{
path: '/device',
name: 'device',
children:[
{
path: '/points',
name: 'points',
meta: {
title: '点位管理'
},
component: () => import('@/views/device/points.vue')
},
{
path: '/locations',
name: 'locations',
meta: {
title: '货位管理'
},
component: () => import('@/views/device/locations.vue')
},
{
path: '/vehicles',
name: 'vehicles',
meta: {
title: '车辆管理'
},
component: () => import('@/views/device/vehicles.vue')
},
{
path: '/chargers',
name: 'chargers',
meta: {
title: '充电位管理'
},
component: () => import('@/views/device/chargers.vue')
}
]
},
{
path: '/inventory',
name: 'inventory',
children:[
{
path: '/query',
name: 'query',
meta: {
title: '库存查询'
},
component: () => import('@/views/inventory/query.vue')
},
{
path: '/account',
name: 'account',
meta: {
title: '帐页查询'
},
component: () => import('@/views/inventory/account.vue')
}
]
},
{
path: '/user',
name: 'user',
children:[
{
path: '/users',
name: 'users',
meta: {
title: '用户管理'
},
component: () => import('@/views/user/users.vue')
},
{
path: '/roles',
name: 'roles',
meta: {
title: '角色管理'
},
component: () => import('@/views/user/roles.vue')
}
]
}
]
}, },
{ {
path: '/tp', path: '/tp',

74
src/views/Header.vue

@ -0,0 +1,74 @@
<template>
<div class="header-wrapper">
<div class="left">
<div class="logo"><img :src="Logo" alt="" style="height: 30px;width: 169px"></div>
<span class="menu-icon" @click="handleToggle">
<!-- 使用 props.collapsed -->
<component v-if="!props.collapsed" :is="renderIcon('antd MenuFoldOutlined')"></component>
<component v-else :is="renderIcon('antd MenuUnfoldOutlined')"></component>
</span>
</div>
<div class="user">
<span>
<component :is="renderIcon('element User')"></component>
</span>
</div>
</div>
</template>
<script setup>
import {ref} from 'vue'
import { renderIcon } from '@/utils/webutils.js'
import Logo from '@/assets/images/logo.png'
// props
const props = defineProps({
isMobile: Boolean,
collapsed: Boolean //
})
const emit = defineEmits(['toggle-collapse'])
function handleToggle() {
emit('toggle-collapse')
}
</script>
<style lang="less">
.header-wrapper {
display: flex;
flex-direction: row;
overflow: hidden;
justify-content: space-between;
align-items: center;
height: 100%;
.left{
flex:1;
display: flex;
flex-direction: row;
.logo {
display: flex;
align-items: center;
margin: 0 20px 0 10px;
}
.menu-icon{
display: inline-flex;
padding:10px;
cursor: pointer;
.el-icon{
font-size: 20px;
color:#fff;
}
}
}
.user {
display: flex;
flex-direction: row;
align-items: center;
& > span {
display: inline-flex;
padding: 5px;
background: #f4c521;
border-radius: 15px;
color: #fff;
}
}
}
</style>

152
src/views/HomeView.vue

@ -1,50 +1,126 @@
<template> <template>
<div id="sence" ref="threeDomElement" tabindex="1"></div> <div class="layout">
<el-container style="height: 100%;overflow: hidden;">
<!-- 头部 -->
<el-header class="header">
<Header @toggle-collapse="toggleCollapse" :collapsed="collapsed" :is-mobile="isMobile" />
</el-header>
<el-container style="height: 100%;overflow: hidden;">
<!-- 侧边栏 -->
<el-aside v-show="!isMobile || !collapsed" :width="collapsed ? '64px' : '200px'" class="sidebar">
<Sidebar :collapsed="collapsed" />
</el-aside>
<!-- 内容 -->
<el-main class="main">
<el-breadcrumb separator="/">
<el-breadcrumb-item>
{{route.meta?.title}}
</el-breadcrumb-item>
</el-breadcrumb>
<div class="content">
<router-view />
</div>
</el-main>
</el-container>
</el-container>
</div>
<!-- 移动端遮罩层 -->
<div v-if="isMobile" v-show="!collapsed" class="mask" @click="toggleCollapse"></div>
</template> </template>
<script setup>
import { onMounted, ref } from 'vue'
import * as THREE from 'three'
import { GUI } from 'dat.gui'
import _ from 'lodash'
const threeDomElement = ref(null)
onMounted(() => {
// lodash <script setup>
const arr = [1, 2, 3] import { ref, computed, onMounted} from 'vue'
console.log(_.reverse(arr)) import {useRoute} from "vue-router";
import Sidebar from './Sidebar.vue'
import Header from './Header.vue'
const route = useRoute()
console.log(route.meta?.title)
const collapsed = ref(false)
const isMobile = ref(false)
// Three.js // /
const scene = new THREE.Scene() function toggleCollapse() {
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) collapsed.value = !collapsed.value
}
const renderer = new THREE.WebGLRenderer() //
threeDomElement.value.appendChild(renderer.domElement) function checkIsMobile() {
renderer.setSize(window.innerWidth, window.innerHeight) isMobile.value = window.innerWidth < 768
if (isMobile.value) {
collapsed.value = true //
}
}
const geometry = new THREE.BoxGeometry() onMounted(() => {
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }) checkIsMobile()
const cube = new THREE.Mesh(geometry, material) window.addEventListener('resize', checkIsMobile)
scene.add(cube) })
camera.position.z = 5 </script>
function animate() { <style scoped lang="less">
requestAnimationFrame(animate) .layout {
cube.rotation.x += 0.01 height: 100vh;
cube.rotation.y += 0.01 }
renderer.render(scene, camera) .header {
height: 50px;
background: #545c64;
flex-shrink: 0;
padding:0 10px;
}
.sidebar {
transition: width 0.3s ease;
}
.main {
padding: 10px;
background: #f6f7fb;
display: flex;
flex-direction: column;
overflow: hidden;
width: 100%;
height: 100%;
.el-breadcrumb{
margin:0 0 10px 10px;
} }
&>.content{
flex: 1;
overflow: hidden;
}
}
.mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,.4);
z-index: 999;
}
@media (max-width: 768px) {
.layout {
flex-direction: column;
}
.el-aside {
position: fixed;
top: 0;
left: 0;
height: 100%;
z-index: 1000;
transform: translateX(0);
transition: transform 0.6s ease;
background: #fff;
animate() &.open {
transform: translateX(0);
}
}
// Dat.GUI .el-aside[style*='transform: translateX(0px)'] {
const gui = new GUI() transform: translateX(0);
const cubeFolder = gui.addFolder('Cube') }
cubeFolder.add(cube.rotation, 'x', 0, Math.PI * 2)
cubeFolder.add(cube.rotation, 'y', 0, Math.PI * 2)
cubeFolder.open()
})
</script> .el-aside[style*='transform: translateX(-100%)'] {
transform: translateX(-100%);
}
}
</style>

85
src/views/Sidebar.vue

@ -0,0 +1,85 @@
<template>
<el-menu
default-active="dashboard"
class="menu"
:collapse="collapsed"
:collapse-transition="false"
router
>
<el-menu-item index="/dashboard">
<component :is="renderIcon('antd DashboardOutlined')"></component>
<!-- antd BarChartOutlined-->
<span>仪表盘</span>
</el-menu-item>
<el-menu-item index="/modelingSimulation">
<component :is="renderIcon('antd BankFilled')"></component>
<span>建模仿真控制平台</span>
</el-menu-item>
<el-sub-menu index="/taskManagement">
<template #title>
<component :is="renderIcon('fa Tasks')"></component><span>任务管理</span></template>
<el-menu-item index="/taskQuery">
<component :is="renderIcon('antd FileSearchOutlined')"></component>任务查询</el-menu-item>
<el-menu-item index="/automatedPresentation">
<component :is="renderIcon('fa ChalkboardTeacher')"></component>自动演示管理</el-menu-item>
</el-sub-menu>
<el-sub-menu index="/log">
<template #title>
<component :is="renderIcon('antd FileSearchOutlined')"></component>
<span>日志查询</span>
</template>
<el-menu-item index="/upstream">
<component :is="renderIcon('element Memo')"></component>上游接口日志</el-menu-item>
<el-menu-item index="/device">
<component :is="renderIcon('element MessageBox')"></component>设备报文日志</el-menu-item>
</el-sub-menu>
<el-sub-menu index="/device">
<template #title>
<component :is="renderIcon('antd DatabaseOutlined')"></component>
<span>设备管理</span>
</template>
<el-menu-item index="/points">
<component :is="renderIcon('antd EnvironmentOutlined')"></component>点位管理</el-menu-item>
<el-menu-item index="/locations">
<component :is="renderIcon('antd BorderOuterOutlined')"></component>货位管理</el-menu-item>
<el-menu-item index="/vehicles">
<component :is="renderIcon('antd CarOutlined')"></component>车辆管理</el-menu-item>
<el-menu-item index="/chargers">
<component :is="renderIcon('antd ThunderboltOutlined')"></component>充电位管理</el-menu-item>
</el-sub-menu>
<el-sub-menu index="/inventory">
<template #title>
<component :is="renderIcon('fa EditRegular')"></component>
<span>库存管理</span>
</template>
<el-menu-item index="/query">
<component :is="renderIcon('antd FileSearchOutlined')"></component>库存查询</el-menu-item>
<el-menu-item index="/account">
<component :is="renderIcon('antd SearchOutlined')"></component>帐页查询</el-menu-item>
</el-sub-menu>
<el-sub-menu index="/user">
<template #title>
<component :is="renderIcon('element User')"></component>
<span>用户管理</span>
</template>
<el-menu-item index="/users">
<component :is="renderIcon('antd UserOutlined')"></component>用户管理</el-menu-item>
<el-menu-item index="/roles">
<component :is="renderIcon('antd UserSwitchOutlined')"></component>角色管理</el-menu-item>
</el-sub-menu>
</el-menu>
</template>
<script setup>
import { renderIcon } from '@/utils/webutils.js'
defineProps({
collapsed: Boolean
})
</script>
<style scoped>
.menu {
border-right: none;
height: 100%;
}
</style>

58
src/views/dashboard/EChartWrapper.vue

@ -0,0 +1,58 @@
<template>
<div ref="chart" class="chart"></div>
</template>
<script setup>
import * as echarts from 'echarts';
import { ref, onMounted, watch,nextTick} from 'vue';
import { useResizeObserver } from '@vueuse/core';
const props=defineProps({
options:{
type:Object,
required:true
}
})
const chart =ref(null);
let chartInstance=null;//
//
const initChart=()=>{
if(chartInstance) chartInstance.dispose()
chartInstance=echarts.init(chart.value)
chartInstance.setOption(props.options)
}
//
const updateChart = (newOptions) => {
if (chartInstance) {
chartInstance.setOption(newOptions);
}
};
//
const handleResize=()=>{
if(chartInstance) chartInstance.resize();
}
//options
watch(()=>props.options,(newOptions)=>{
if(chartInstance) chartInstance.setOption(newOptions, true)
},{ deep: true })
onMounted(() => {
nextTick(() => {
initChart();
useResizeObserver(chart, handleResize);
});
});
//
defineExpose({
updateChart
});
</script>
<style scoped>
.chart {
height: 100%;
}
</style>

280
src/views/dashboard/index.vue

@ -0,0 +1,280 @@
<template>
<div class="dashboard">
<div class="stat-row">
<div class="stat-card purple">
<div class="title">穿梭板数</div>
<div class="number">11</div>
</div>
<div class="stat-card blue">
<div class="title">提升机数</div>
<div class="number">16</div>
</div>
<div class="stat-card red">
<div class="title">使用库位数/总库位数</div>
<div class="number">1864/8404</div>
</div>
<div class="stat-card green">
<div class="title">存储率</div>
<div class="number">22%</div>
</div>
</div>
<el-row :gutter="10">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-card class="chart-card">
<template #header>
<div class="card-header">
<span>设备任务状态占比</span>
</div>
</template>
<div class="chart-wrap">
<EChartWrapper :options="option1"></EChartWrapper>
</div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
<el-card class="chart-card">
<template #header>
<div class="card-header">
<span>业务任务趋势</span>
</div>
</template>
<div class="chart-wrap">
<EChartWrapper :options="option2"></EChartWrapper>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import * as echarts from 'echarts';
import { ref, onMounted, watch,nextTick} from 'vue';
import EChartWrapper from './EChartWrapper.vue'
const option1=ref({
tooltip: {
trigger: 'axis',
formatter: (params) => {
// params
let result = `${params[0].name}<br/>`; // x
params.forEach((item) => {
result += `${item.seriesName}: ${item.value || 0}单<br/>`;
});
return result;
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: (() => {
const dates = []
for (let i = 6; i >= 0; i--) {
const date = new Date()
date.setDate(date.getDate() - i)
dates.push(date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }))
}
return dates
})()
},
yAxis: {
type: 'value',
name: '单',
nameTextStyle: {
padding: [0, 0, 0, 30]
},
},
series: [{
name: '1F',
type: 'bar',
data: [120, 150, 80, 160, 10, 270, 110],
itemStyle: {
color: '#409EFF'
},
},{
name: '2F',
type: 'bar',
data: [110, 50, 10, 160, 140, 120, 130],
itemStyle: {
color: '#1bc042'
},
},{
name: '3F',
type: 'bar',
data: [20, 100, 120, 160, 40, 170, 100],
itemStyle: {
color: '#ffa640'
},
}]
})
const option2=ref({
tooltip: {
trigger: 'axis',
formatter: (params) => {
// params
let result = `${params[0].name}<br/>`; // x
params.forEach((item) => {
result += `${item.seriesName}: ${item.value || 0}单<br/>`;
});
return result;
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: (() => {
const dates = []
for (let i = 6; i >= 0; i--) {
const date = new Date()
date.setDate(date.getDate() - i)
dates.push(date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }))
}
return dates
})()
},
yAxis: {
type: 'value',
name: '单',
nameTextStyle: {
padding: [0, 0, 0, 30]
},
},
series: [{
name: '1F',
type: 'bar',
data: [120, 150, 80, 160, 10, 270, 110],
itemStyle: {
color: '#409EFF'
},
},{
name: '2F',
type: 'bar',
data: [110, 50, 10, 160, 140, 120, 130],
itemStyle: {
color: '#1bc042'
},
},{
name: '3F',
type: 'bar',
data: [20, 100, 120, 160, 40, 170, 100],
itemStyle: {
color: '#ffa640'
},
}]
})
</script>
<style lang="less">
.dashboard{
height: 100%;
overflow-x: hidden;
overflow-y: auto;
.stat-row{
height: 120px;
display: flex;
flex-direction: row;
margin-bottom: 20px;
gap: 20px;
.stat-card {
flex:1;
height: 120px;
padding: 20px;
border-radius: 8px;
color: #fff;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
transition: all 0.3s;
&:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px 0 rgba(0,0,0,0.15);
}
.title {
font-size: 14px;
opacity: 0.9;
}
.number {
font-size: 28px;
font-weight: bold;
margin-top: 10px;
}
&.purple {
background: linear-gradient(135deg, #5e71fc 0%, #aa7bfc 100%);
}
&.blue {
background: linear-gradient(135deg, #3da2f5 0%, #6885fa 100%);
}
&.red {
background: linear-gradient(135deg, #ea6274 0%, #f09165 100%);
}
&.green {
background: linear-gradient(135deg, #46c0ab 0%, #7ceba6 100%);
}
&.orange {
background: linear-gradient(135deg, #efa03b 0%, #fad46f 100%);
}
}
}
.chart-card {
background-color: #fff;
border-radius: 4px;
:deep(.el-card__header){
padding:0;
border-bottom: 0;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
span {
font-size: 14px;
}
}
.chart {
height: 300px;
}
:deep(.date-picker){
width: 100px;
}
.chart-wrap {
height: 300px;
}
}
.el-card__header{
padding:0
}
}
@media (max-width: 768px) {
.stat-row {
overflow: auto;
}
.el-col-xs-24 + .el-col-xs-24{
margin-top:20px;
}
}
</style>

12
src/views/device/chargers.vue

@ -0,0 +1,12 @@
<template>
<div class="modeling-simulation">
chargers
</div>
</template>
<script setup>
</script>
<style lang="less">
.dashboard{
}
</style>

12
src/views/device/locations.vue

@ -0,0 +1,12 @@
<template>
<div class="modeling-simulation">
locations
</div>
</template>
<script setup>
</script>
<style lang="less">
.dashboard{
}
</style>

12
src/views/device/points.vue

@ -0,0 +1,12 @@
<template>
<div class="modeling-simulation">
points
</div>
</template>
<script setup>
</script>
<style lang="less">
.dashboard{
}
</style>

12
src/views/device/vehicles.vue

@ -0,0 +1,12 @@
<template>
<div class="modeling-simulation">
vehicles
</div>
</template>
<script setup>
</script>
<style lang="less">
.dashboard{
}
</style>

12
src/views/inventory/account.vue

@ -0,0 +1,12 @@
<template>
<div class="dashboard">
dashboard
</div>
</template>
<script setup>
</script>
<style lang="less">
.dashboard{
}
</style>

12
src/views/inventory/query.vue

@ -0,0 +1,12 @@
<template>
<div class="modeling-simulation">
query
</div>
</template>
<script setup>
</script>
<style lang="less">
.dashboard{
}
</style>

12
src/views/log/device.vue

@ -0,0 +1,12 @@
<template>
<div class="modeling-simulation">
device
</div>
</template>
<script setup>
</script>
<style lang="less">
.dashboard{
}
</style>

12
src/views/log/upstream.vue

@ -0,0 +1,12 @@
<template>
<div class="modeling-simulation">
upstream
</div>
</template>
<script setup>
</script>
<style lang="less">
.dashboard{
}
</style>

12
src/views/modelingSimulation/index.vue

@ -0,0 +1,12 @@
<template>
<div class="modeling-simulation">
modelingSimulation
</div>
</template>
<script setup>
</script>
<style lang="less">
.dashboard{
}
</style>

12
src/views/taskManagement/automatedPresentation.vue

@ -0,0 +1,12 @@
<template>
<div class="modeling-simulation">
automatedPresentation.vue
</div>
</template>
<script setup>
</script>
<style lang="less">
.dashboard{
}
</style>

12
src/views/taskManagement/taskQuery.vue

@ -0,0 +1,12 @@
<template>
<div class="modeling-simulation">
taskQuery.vue
</div>
</template>
<script setup>
</script>
<style lang="less">
.dashboard{
}
</style>

12
src/views/user/roles.vue

@ -0,0 +1,12 @@
<template>
<div class="roles">
roles
</div>
</template>
<script setup>
</script>
<style lang="less">
.dashboard{
}
</style>

12
src/views/user/users.vue

@ -0,0 +1,12 @@
<template>
<div class="dashboard">
users
</div>
</template>
<script setup>
</script>
<style lang="less">
.dashboard{
}
</style>
Loading…
Cancel
Save