30 changed files with 1487 additions and 84 deletions
|
After Width: | Height: | Size: 2.5 KiB |
@ -0,0 +1,61 @@ |
|||||
|
import type { InternalAxiosRequestConfig } from "axios"; |
||||
|
import { Constant, initGlobalConfig } from "@ease-forge/shared"; |
||||
|
import { initGlobalConfigWithRuntime } from "@ease-forge/runtime"; |
||||
|
import router from "@/router"; |
||||
|
|
||||
|
function globalConfig() { |
||||
|
window.globalConfig.customAxios = axiosInstance => { |
||||
|
// 全局请求拦截
|
||||
|
axiosInstance.interceptors.request.clear(); |
||||
|
axiosInstance.interceptors.request.use( |
||||
|
(request: InternalAxiosRequestConfig) => request, |
||||
|
(error: any) => { |
||||
|
const err: AxiosInterceptorError = { |
||||
|
rawError: error, |
||||
|
title: "系统错误", |
||||
|
message: "发送请求给服务端失败,请检查电脑网络,再重试", |
||||
|
status: -1, |
||||
|
}; |
||||
|
return Promise.reject(err); |
||||
|
}, |
||||
|
); |
||||
|
// 全局拦截配置
|
||||
|
axiosInstance.interceptors.response.clear(); |
||||
|
axiosInstance.interceptors.response.use( |
||||
|
response => response, |
||||
|
(error: any) => { |
||||
|
const { response } = error; |
||||
|
const err: AxiosInterceptorError = { |
||||
|
rawError: error, |
||||
|
status: response?.status ?? -1, |
||||
|
title: "系统错误", |
||||
|
message: "", |
||||
|
}; |
||||
|
if (!error || !response) { |
||||
|
err.message = "请求服务端异常"; |
||||
|
} else if (response?.status === 401) { |
||||
|
err.title = "当前用户未登录"; |
||||
|
err.message = "当前用户未登录,请先登录系统"; |
||||
|
router.push({ name: "login" }).finally(); |
||||
|
} else { |
||||
|
err.title = "操作失败"; |
||||
|
const { data: { message, validMessageList } } = response; |
||||
|
if (validMessageList) { |
||||
|
err.message = "请求参数校验失败"; |
||||
|
} else if (message) { |
||||
|
err.message = message ?? Constant.defHttpErrorMsg[response.status] ?? "服务器异常"; |
||||
|
} |
||||
|
} |
||||
|
system.msg(err.message); |
||||
|
return Promise.reject(err); |
||||
|
}, |
||||
|
); |
||||
|
return axiosInstance; |
||||
|
}; |
||||
|
initGlobalConfig(); |
||||
|
initGlobalConfigWithRuntime(); |
||||
|
} |
||||
|
|
||||
|
export { |
||||
|
globalConfig, |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
import lodash from "lodash"; |
||||
|
import { Request } from "@ease-forge/shared"; |
||||
|
|
||||
|
async function getCurrentUser() { |
||||
|
return Request.request.post("/api/current_user").then(res => { |
||||
|
const userInfo = res.userInfo; |
||||
|
const { loginName, userId, userName } = userInfo; |
||||
|
window.globalConfig.user = { |
||||
|
uid: lodash.toString(userId), |
||||
|
loginName: loginName, |
||||
|
nickname: userName, |
||||
|
}; |
||||
|
window.globalConfig.security.roles.length = 0; |
||||
|
window.globalConfig.security.permissions.length = 0; |
||||
|
window.globalConfig.security.roles.push(...res.roles); |
||||
|
window.globalConfig.security.permissions.push(...res.permissions); |
||||
|
}).catch(err => { |
||||
|
console.log("未登录", err); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
export { |
||||
|
getCurrentUser |
||||
|
} |
||||
@ -0,0 +1,263 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { computed, createVNode, reactive, useTemplateRef } from "vue"; |
||||
|
import { ElButton, ElSpace, ElTree } from "element-plus"; |
||||
|
import YvSrcEditor from "@/components/YvSrcEditor.vue"; |
||||
|
import DataForm from "@/components/data-form/DataForm.vue"; |
||||
|
import lodash from "lodash"; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'CatalogDefine', |
||||
|
}); |
||||
|
|
||||
|
// 组件事件定义 |
||||
|
// const emit = defineEmits<{ |
||||
|
// /** 更新内联表格数据 */ |
||||
|
// "event01": [param01: string]; |
||||
|
// }>(); |
||||
|
|
||||
|
// 定义 Props 类型 |
||||
|
interface CatalogDefineProps { |
||||
|
} |
||||
|
|
||||
|
// 读取组件 props 属性 |
||||
|
const props = withDefaults(defineProps<CatalogDefineProps>(), {}); |
||||
|
|
||||
|
// 定义 State 类型 |
||||
|
interface CatalogDefineState { |
||||
|
// forceUpdateForCatalog: number; |
||||
|
} |
||||
|
|
||||
|
// state 属性 |
||||
|
const state = reactive<CatalogDefineState>({ |
||||
|
// forceUpdateForCatalog: Number.MIN_VALUE, |
||||
|
}); |
||||
|
|
||||
|
// 定义 Data 类型 |
||||
|
interface CatalogDefineData { |
||||
|
} |
||||
|
|
||||
|
// 内部数据 |
||||
|
const data: CatalogDefineData = {}; |
||||
|
const tree = useTemplateRef<InstanceType<typeof ElTree>>("treeRef"); |
||||
|
const worldModel = computed(() => window['worldModel']); |
||||
|
const catalog = computed<Array<any>>(() => { |
||||
|
// state.forceUpdateForCatalog; |
||||
|
return worldModel.value?.state?.catalog; |
||||
|
}); |
||||
|
const catalogTree = computed(() => { |
||||
|
const array = catalog.value; |
||||
|
const tree: Array<any> = []; |
||||
|
if (array) { |
||||
|
for (let item of array) { |
||||
|
const node: any = { |
||||
|
id: item.label, |
||||
|
label: item.label, |
||||
|
data: item, |
||||
|
}; |
||||
|
tree.push(node); |
||||
|
if (item.items && item.items.length > 0) { |
||||
|
node.children = []; |
||||
|
for (let row of item.items) { |
||||
|
const child = { |
||||
|
pid: item.label, |
||||
|
id: `${item.label}_${row.catalogCode}`, |
||||
|
label: row.label, |
||||
|
data: row, |
||||
|
}; |
||||
|
node.children.push(child); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return tree; |
||||
|
}); |
||||
|
const catalogJson = computed(() => { |
||||
|
if (catalog.value) return JSON.stringify(catalog.value, null, 4); |
||||
|
return ""; |
||||
|
}); |
||||
|
|
||||
|
function addCatalog() { |
||||
|
const data = { |
||||
|
label: "", |
||||
|
}; |
||||
|
system.showDialog(createVNode(DataForm, { |
||||
|
style: { |
||||
|
paddingRight: "12px", |
||||
|
}, |
||||
|
data: data, |
||||
|
formFields: [ |
||||
|
{ |
||||
|
dataPath: 'label', label: '目录名称', input: 'Input', |
||||
|
inputProps: { |
||||
|
placeholder: '目录名称', |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
columnCount: 1, |
||||
|
labelWidth: "80px", |
||||
|
}), { |
||||
|
title: '添加目录', |
||||
|
width: 480, |
||||
|
height: 150, |
||||
|
showClose: true, |
||||
|
showMax: false, |
||||
|
showCancelButton: true, |
||||
|
showOkButton: true, |
||||
|
okButtonText: "确定", |
||||
|
cancelButtonText: "取消", |
||||
|
}).then(() => { |
||||
|
const label = lodash.trim(data.label); |
||||
|
if (!label) { |
||||
|
system.msg("目录名称不能为空"); |
||||
|
return; |
||||
|
} |
||||
|
catalog.value.push({ label, items: [] }); |
||||
|
}).finally(); |
||||
|
} |
||||
|
|
||||
|
function addItem() { |
||||
|
const node = tree.value?.getCurrentNode(); |
||||
|
const catalogData = node?.data; |
||||
|
if (!catalogData?.items) { |
||||
|
system.msg("必须先选择一个目录"); |
||||
|
return; |
||||
|
} |
||||
|
// console.log("node", node); |
||||
|
const data = { |
||||
|
label: "", |
||||
|
catalogCode: "", |
||||
|
}; |
||||
|
system.showDialog(createVNode(DataForm, { |
||||
|
style: { |
||||
|
paddingRight: "12px", |
||||
|
}, |
||||
|
data: data, |
||||
|
formFields: [ |
||||
|
{ |
||||
|
dataPath: 'catalogCode', label: '楼层编码', input: 'Input', |
||||
|
inputProps: { |
||||
|
placeholder: '楼层唯一编码', |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
dataPath: 'label', label: '楼层名称', input: 'Input', |
||||
|
inputProps: { |
||||
|
placeholder: '楼层名称', |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
columnCount: 1, |
||||
|
labelWidth: "80px", |
||||
|
}), { |
||||
|
title: '添加楼层', |
||||
|
width: 480, |
||||
|
height: 150, |
||||
|
showClose: true, |
||||
|
showMax: false, |
||||
|
showCancelButton: true, |
||||
|
showOkButton: true, |
||||
|
okButtonText: "确定", |
||||
|
cancelButtonText: "取消", |
||||
|
}).then(() => { |
||||
|
const catalogCode = lodash.trim(data.catalogCode); |
||||
|
const label = lodash.trim(data.label); |
||||
|
if (!catalogCode) { |
||||
|
system.msg("楼层编码不能为空"); |
||||
|
return; |
||||
|
} |
||||
|
if (!label) { |
||||
|
system.msg("楼层名称不能为空"); |
||||
|
return; |
||||
|
} |
||||
|
catalogData.items.push({ label, catalogCode }); |
||||
|
}).finally(); |
||||
|
} |
||||
|
|
||||
|
function del() { |
||||
|
const node = tree.value?.getCurrentNode(); |
||||
|
if (!node) { |
||||
|
system.msg("必须先选择一个节点"); |
||||
|
return; |
||||
|
} |
||||
|
// console.log("node", node); |
||||
|
const nodeData = node.data; |
||||
|
if (node.pid) { |
||||
|
let index = catalog.value.findIndex(item => item.label === node.pid); |
||||
|
if (index >= 0) { |
||||
|
const catalogData = catalog.value[index]; |
||||
|
index = catalogData.items.findIndex(item => item.catalogCode === nodeData.catalogCode); |
||||
|
if (index >= 0) { |
||||
|
catalogData.items.splice(index, 1); |
||||
|
// state.forceUpdateForCatalog++; |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
const index = catalog.value.findIndex(item => item.label === nodeData.label); |
||||
|
if (index >= 0) { |
||||
|
catalog.value.splice(index, 1); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
interface CatalogDefineExpose { |
||||
|
state: CatalogDefineState; |
||||
|
data: CatalogDefineData; |
||||
|
} |
||||
|
|
||||
|
const expose: CatalogDefineExpose = { |
||||
|
state, |
||||
|
data, |
||||
|
}; |
||||
|
// 定义组件公开内容 |
||||
|
defineExpose(expose); |
||||
|
|
||||
|
export type { |
||||
|
CatalogDefineProps, |
||||
|
CatalogDefineState, |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="flex-row-container root"> |
||||
|
<div class="flex-item-fixed flex-column-container left"> |
||||
|
<ElSpace class="flex-item-fixed tools-top"> |
||||
|
<ElButton @click="addCatalog" :disabled="!catalog">添加目录</ElButton> |
||||
|
<ElButton @click="addItem" :disabled="!catalog">添加楼层</ElButton> |
||||
|
<ElButton @click="del" :disabled="!catalog">删除</ElButton> |
||||
|
</ElSpace> |
||||
|
<div class="catalog-tree"> |
||||
|
<ElTree |
||||
|
ref="treeRef" |
||||
|
:data="catalogTree" |
||||
|
nodeKey="id" |
||||
|
:expandOnClickNode="false" |
||||
|
:highlightCurrent="true" |
||||
|
:defaultExpandAll="true" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="flex-item-fill"> |
||||
|
<YvSrcEditor ref="editorRef" language="json" :modelValue="catalogJson"/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.root { |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.left { |
||||
|
width: 300px; |
||||
|
border-right: 1px solid #ece2e2; |
||||
|
} |
||||
|
|
||||
|
.tools-top { |
||||
|
padding: 8px; |
||||
|
border-bottom: 1px solid #ece2e2; |
||||
|
} |
||||
|
|
||||
|
.catalog-tree { |
||||
|
|
||||
|
} |
||||
|
</style> |
||||
@ -1,34 +1,31 @@ |
|||||
import { createApp } from 'vue' |
import { createApp } from 'vue' |
||||
import { createPinia } from 'pinia' |
import { createPinia } from 'pinia' |
||||
|
|
||||
import App from './App.vue' |
import App from './App.vue' |
||||
import router from './router' |
import router from './router' |
||||
import * as webIndex from '@/components/webindex' |
import * as webIndex from '@/components/webindex' |
||||
import { directive, menusEvent, Vue3Menus } from 'vue3-menus' |
import { directive, menusEvent, Vue3Menus } from 'vue3-menus' |
||||
import ElementPlus from 'element-plus' |
import ElementPlus from 'element-plus' |
||||
import { initGlobalConfig } from "@ease-forge/shared"; |
import { globalConfig } from "@/config.ts"; |
||||
import { initGlobalConfigWithRuntime } from "@ease-forge/runtime"; |
|
||||
import System from '@/runtime/System' |
import System from '@/runtime/System' |
||||
|
import { getCurrentUser } from "@/currentUser.ts"; |
||||
import 'ag-grid-community/styles/ag-grid.css' |
import 'ag-grid-community/styles/ag-grid.css' |
||||
import 'ag-grid-community/styles/ag-theme-alpine.css' |
import 'ag-grid-community/styles/ag-theme-alpine.css' |
||||
import 'element-plus/dist/index.css' |
import 'element-plus/dist/index.css' |
||||
import './main.less' |
import './main.less' |
||||
|
|
||||
initGlobalConfig(); |
async function main() { |
||||
initGlobalConfigWithRuntime(); |
const app = createApp(App) |
||||
|
app.use(createPinia()) |
||||
const app = createApp(App) |
app.use(ElementPlus) |
||||
|
app.component('vue3-menus', Vue3Menus) |
||||
app.use(createPinia()) |
app.directive('menus', directive) |
||||
app.use(ElementPlus) |
app.config.globalProperties.$menusEvent = menusEvent |
||||
app.component('vue3-menus', Vue3Menus) |
window['system'] = new System(app) |
||||
app.directive('menus', directive) |
app.use(router) |
||||
app.config.globalProperties.$menusEvent = menusEvent |
app.use(webIndex) |
||||
|
app.mount('#app') |
||||
window['system'] = new System(app) |
globalConfig(); |
||||
|
await getCurrentUser(); |
||||
app.use(router) |
} |
||||
app.use(webIndex) |
|
||||
|
main().finally(); |
||||
app.mount('#app') |
|
||||
|
|||||
@ -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> |
||||
@ -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> |
||||
|
|||||
@ -0,0 +1,181 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { markRaw, reactive, useTemplateRef } from "vue"; |
||||
|
import { useRouter } from "vue-router"; |
||||
|
import { ElButton, type FormRules } from "element-plus"; |
||||
|
import { Lock, User } from "@element-plus/icons-vue"; |
||||
|
import { Request } from "@ease-forge/shared"; |
||||
|
import DataForm from "../components/data-form/DataForm.vue"; |
||||
|
import { type FormField } from "../components/data-form/DataFormTypes.ts"; |
||||
|
import { getCurrentUser } from "@/currentUser.ts"; |
||||
|
|
||||
|
defineOptions({ |
||||
|
name: 'Login', |
||||
|
}); |
||||
|
|
||||
|
// 定义 Props 类型 |
||||
|
interface LoginProps { |
||||
|
} |
||||
|
|
||||
|
// 读取组件 props 属性 |
||||
|
const props = withDefaults(defineProps<LoginProps>(), {}); |
||||
|
|
||||
|
// 定义 State 类型 |
||||
|
interface LoginState { |
||||
|
loginName?: string; |
||||
|
password?: string; |
||||
|
loading?: boolean; |
||||
|
errMsg?: string; |
||||
|
} |
||||
|
|
||||
|
// state 属性 |
||||
|
const state = reactive<LoginState>({}); |
||||
|
|
||||
|
// 定义 Data 类型 |
||||
|
interface LoginData { |
||||
|
formFields: Array<FormField>; |
||||
|
rules: FormRules, |
||||
|
} |
||||
|
|
||||
|
// 内部数据 |
||||
|
const data: LoginData = { |
||||
|
formFields: [ |
||||
|
{ |
||||
|
dataPath: 'loginName', label: '', input: 'Input', inputRef: "loginName", |
||||
|
inputProps: { |
||||
|
placeholder: '用户名', |
||||
|
clearable: true, |
||||
|
prefixIcon: markRaw(User), |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
dataPath: 'password', label: '', input: 'Input', inputRef: "password", |
||||
|
inputProps: { |
||||
|
placeholder: '登录密码', |
||||
|
type: "password", |
||||
|
showPassword: true, |
||||
|
prefixIcon: markRaw(Lock), |
||||
|
}, |
||||
|
}, |
||||
|
], |
||||
|
rules: { |
||||
|
loginName: [ |
||||
|
{ required: true, message: "用户名必填", trigger: 'blur' }, |
||||
|
], |
||||
|
password: [ |
||||
|
{ required: true, message: "登录密码必填", trigger: 'blur' }, |
||||
|
], |
||||
|
}, |
||||
|
}; |
||||
|
const form = useTemplateRef<InstanceType<typeof DataForm>>("formRef"); |
||||
|
const router = useRouter(); |
||||
|
|
||||
|
async function login() { |
||||
|
await form.value.data.formRef.validate(valid => { |
||||
|
if (!valid) return; |
||||
|
doLogin().finally(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
async function doLogin() { |
||||
|
state.loading = true; |
||||
|
state.errMsg = ""; |
||||
|
try { |
||||
|
const res = await Request.request.post("/api/login", { |
||||
|
loginName: state.loginName, |
||||
|
password: state.password, |
||||
|
}); |
||||
|
const userInfo = res.userInfo; |
||||
|
if (userInfo) { |
||||
|
await getCurrentUser(); |
||||
|
await router.push({ name: "editor" }); |
||||
|
system.msg("登录成功"); |
||||
|
} else { |
||||
|
state.errMsg = res.message ?? "登录失败,请重试!"; |
||||
|
} |
||||
|
} catch (err) { |
||||
|
state.errMsg = "登录失败,请重试!"; |
||||
|
} finally { |
||||
|
state.loading = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
interface LoginExpose { |
||||
|
state: LoginState; |
||||
|
data: LoginData; |
||||
|
} |
||||
|
|
||||
|
const expose: LoginExpose = { |
||||
|
state, |
||||
|
data, |
||||
|
}; |
||||
|
// 定义组件公开内容 |
||||
|
defineExpose(expose); |
||||
|
|
||||
|
export type { |
||||
|
LoginProps, |
||||
|
LoginState, |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="login-container content-center"> |
||||
|
<div class="login-form-container"> |
||||
|
<div class="login-title">登录</div> |
||||
|
<DataForm |
||||
|
ref="formRef" |
||||
|
class="login-form" |
||||
|
:size="'large'" |
||||
|
:data="state" |
||||
|
:formFields="data.formFields" |
||||
|
:rules="data.rules" |
||||
|
:showMessage="true" |
||||
|
:columnCount="1" |
||||
|
labelWidth="60px" |
||||
|
inputWidth="" |
||||
|
> |
||||
|
<template #submit> |
||||
|
<ElButton class="login-button" :loading="state.loading" type="primary" @click="login"> |
||||
|
登录 |
||||
|
</ElButton> |
||||
|
<div class="login-error">{{ state.errMsg }}</div> |
||||
|
</template> |
||||
|
</DataForm> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style scoped> |
||||
|
.login-container { |
||||
|
height: 100%; |
||||
|
background-color: #f2f5f7; |
||||
|
} |
||||
|
|
||||
|
.login-form-container { |
||||
|
width: 396px; |
||||
|
height: 360px; |
||||
|
padding: 32px 48px 24px 48px; |
||||
|
background-color: #ffffff; |
||||
|
} |
||||
|
|
||||
|
.login-title { |
||||
|
font-size: 40px; |
||||
|
color: #409eff; |
||||
|
font-weight: 500; |
||||
|
margin-bottom: 40px; |
||||
|
} |
||||
|
|
||||
|
.login-form { |
||||
|
height: 200px; |
||||
|
} |
||||
|
|
||||
|
.login-error { |
||||
|
color: #ff4d4f; |
||||
|
width: 100%; |
||||
|
height: 24px; |
||||
|
line-height: 24px; |
||||
|
} |
||||
|
|
||||
|
.login-button { |
||||
|
width: 100%; |
||||
|
} |
||||
|
</style> |
||||
@ -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> |
||||
@ -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> |
||||
@ -0,0 +1,281 @@ |
|||||
|
<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; |
||||
|
border: none; |
||||
|
} |
||||
|
} |
||||
|
@media (max-width: 768px) { |
||||
|
.stat-row { |
||||
|
overflow: auto; |
||||
|
} |
||||
|
.el-col-xs-24 + .el-col-xs-24{ |
||||
|
margin-top:20px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,12 @@ |
|||||
|
<template> |
||||
|
<div class="modeling-simulation"> |
||||
|
chargers |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
.dashboard{ |
||||
|
|
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,12 @@ |
|||||
|
<template> |
||||
|
<div class="modeling-simulation"> |
||||
|
locations |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
.dashboard{ |
||||
|
|
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,12 @@ |
|||||
|
<template> |
||||
|
<div class="modeling-simulation"> |
||||
|
points |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
.dashboard{ |
||||
|
|
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,12 @@ |
|||||
|
<template> |
||||
|
<div class="modeling-simulation"> |
||||
|
vehicles |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
.dashboard{ |
||||
|
|
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,12 @@ |
|||||
|
<template> |
||||
|
<div class="dashboard"> |
||||
|
dashboard |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
.dashboard{ |
||||
|
|
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,12 @@ |
|||||
|
<template> |
||||
|
<div class="modeling-simulation"> |
||||
|
query |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
.dashboard{ |
||||
|
|
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,12 @@ |
|||||
|
<template> |
||||
|
<div class="modeling-simulation"> |
||||
|
device |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
.dashboard{ |
||||
|
|
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,12 @@ |
|||||
|
<template> |
||||
|
<div class="modeling-simulation"> |
||||
|
upstream |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
.dashboard{ |
||||
|
|
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,12 @@ |
|||||
|
<template> |
||||
|
<div class="modeling-simulation"> |
||||
|
modelingSimulation |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
.dashboard{ |
||||
|
|
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,12 @@ |
|||||
|
<template> |
||||
|
<div class="modeling-simulation"> |
||||
|
automatedPresentation.vue |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
.dashboard{ |
||||
|
|
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,12 @@ |
|||||
|
<template> |
||||
|
<div class="modeling-simulation"> |
||||
|
taskQuery.vue |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
.dashboard{ |
||||
|
|
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,12 @@ |
|||||
|
<template> |
||||
|
<div class="roles"> |
||||
|
roles |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
.dashboard{ |
||||
|
|
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,12 @@ |
|||||
|
<template> |
||||
|
<div class="dashboard"> |
||||
|
users |
||||
|
</div> |
||||
|
</template> |
||||
|
<script setup> |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
.dashboard{ |
||||
|
|
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue