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 { createPinia } from 'pinia' |
|||
|
|||
import App from './App.vue' |
|||
import router from './router' |
|||
import * as webIndex from '@/components/webindex' |
|||
import { directive, menusEvent, Vue3Menus } from 'vue3-menus' |
|||
import ElementPlus from 'element-plus' |
|||
import { initGlobalConfig } from "@ease-forge/shared"; |
|||
import { initGlobalConfigWithRuntime } from "@ease-forge/runtime"; |
|||
import { globalConfig } from "@/config.ts"; |
|||
import System from '@/runtime/System' |
|||
|
|||
import { getCurrentUser } from "@/currentUser.ts"; |
|||
import 'ag-grid-community/styles/ag-grid.css' |
|||
import 'ag-grid-community/styles/ag-theme-alpine.css' |
|||
import 'element-plus/dist/index.css' |
|||
import './main.less' |
|||
|
|||
initGlobalConfig(); |
|||
initGlobalConfigWithRuntime(); |
|||
|
|||
async function main() { |
|||
const app = createApp(App) |
|||
|
|||
app.use(createPinia()) |
|||
app.use(ElementPlus) |
|||
app.component('vue3-menus', Vue3Menus) |
|||
app.directive('menus', directive) |
|||
app.config.globalProperties.$menusEvent = menusEvent |
|||
|
|||
window['system'] = new System(app) |
|||
|
|||
app.use(router) |
|||
app.use(webIndex) |
|||
|
|||
app.mount('#app') |
|||
globalConfig(); |
|||
await getCurrentUser(); |
|||
} |
|||
|
|||
main().finally(); |
|||
|
|||
@ -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> |
|||
<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> |
|||
<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 |
|||
const arr = [1, 2, 3] |
|||
console.log(_.reverse(arr)) |
|||
<script setup> |
|||
import { ref, computed, onMounted} from 'vue' |
|||
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() |
|||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) |
|||
// 切换菜单展开/收起 |
|||
function toggleCollapse() { |
|||
collapsed.value = !collapsed.value |
|||
} |
|||
|
|||
const renderer = new THREE.WebGLRenderer() |
|||
threeDomElement.value.appendChild(renderer.domElement) |
|||
renderer.setSize(window.innerWidth, window.innerHeight) |
|||
// 判断是否是移动设备 |
|||
function checkIsMobile() { |
|||
isMobile.value = window.innerWidth < 768 |
|||
if (isMobile.value) { |
|||
collapsed.value = true // 强制移动端默认关闭 |
|||
} |
|||
} |
|||
|
|||
const geometry = new THREE.BoxGeometry() |
|||
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }) |
|||
const cube = new THREE.Mesh(geometry, material) |
|||
scene.add(cube) |
|||
onMounted(() => { |
|||
checkIsMobile() |
|||
window.addEventListener('resize', checkIsMobile) |
|||
}) |
|||
|
|||
camera.position.z = 5 |
|||
</script> |
|||
|
|||
function animate() { |
|||
requestAnimationFrame(animate) |
|||
cube.rotation.x += 0.01 |
|||
cube.rotation.y += 0.01 |
|||
renderer.render(scene, camera) |
|||
<style scoped lang="less"> |
|||
.layout { |
|||
height: 100vh; |
|||
} |
|||
.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 |
|||
const gui = new GUI() |
|||
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() |
|||
}) |
|||
.el-aside[style*='transform: translateX(0px)'] { |
|||
transform: translateX(0); |
|||
} |
|||
|
|||
</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