Browse Source

ServerView 服务器状态管理、ConnectEnv 连接状态分离、Authorization 服务授权、

master
修宁 5 months ago
parent
commit
fefe376c0c
  1. 51
      src/core/engine/Viewport.ts
  2. 26
      src/core/manager/BackendMessageReceiver.ts
  3. 35
      src/core/manager/EnvManager.ts
  4. 55
      src/core/manager/WorldModel.ts
  5. 84
      src/core/script/LCCScript.ts
  6. 6
      src/editor/ModelMain.less
  7. 110
      src/editor/ModelMain.vue
  8. 14
      src/editor/menus/FileMenu.ts
  9. 5
      src/editor/widgets/IWidgets.ts
  10. 6
      src/editor/widgets/monitor/MonitorView.vue
  11. 8
      src/editor/widgets/script/ScriptMeta.ts
  12. 108
      src/editor/widgets/server/EnvSelectConnect.vue
  13. 2
      src/editor/widgets/server/ServerMeta.ts
  14. 162
      src/editor/widgets/server/ServerView.vue
  15. 6
      src/editor/widgets/task/TaskMeta.ts
  16. 12
      src/editor/widgets/toolbox/ToolboxView.vue
  17. 100
      src/types/LCC.d.ts
  18. 1
      src/types/Model.d.ts
  19. 10
      src/types/Types.d.ts

51
src/core/engine/Viewport.ts

@ -26,9 +26,8 @@ import ItemFindManager from '@/core/manager/ItemFindManager.ts'
import { MapControls } from 'three/examples/jsm/controls/MapControls'
import ModelManager from '@/core/script/ModelManager.ts'
import RuntimeManager from '@/core/manager/RuntimeManager.ts'
import EnvManager from '@/core/manager/EnvManager.ts'
import BackendMessageReceiver from '@/core/manager/BackendMessageReceiver.ts'
import Ammo, {btTransform} from 'ammojs-typed';
import Ammo from 'ammojs-typed'
/**
*
* ,,,,,
@ -42,7 +41,7 @@ export default class Viewport {
raycaster: THREE.Raycaster
animationFrameId: any = null
scene: SceneHelp
ammoModel: Ammo = window.Ammo
ammoModel: any = window.Ammo
physicsWorld: Ammo.btDiscreteDynamicsWorld
transformAux1: Ammo.btTransform
selectManager = new SelectManager()
@ -67,12 +66,12 @@ export default class Viewport {
markRaw(this.itemFindManager),
markRaw(this.interactionManager),
markRaw(this.modelManager),
markRaw(this.runtimeManager),
markRaw(this.runtimeManager)
]
registerFrameTimerCallBack: Map<string, ()=> void> = new Map()
registerFrameTimerCallBack: Map<string, () => void> = new Map()
registerPhysicsUpdateCallBack: Map<string, (deltaTime: number)=> void> = new Map()
registerPhysicsUpdateCallBack: Map<string, (deltaTime: number) => void> = new Map()
// 对象实例管理器 moduleName -> InstanceMeshManager
meshManager: Map<string, InstanceMeshManager> = new Map()
@ -124,9 +123,11 @@ export default class Viewport {
addFrameTimerCallback(id: string, callback: () => void) {
this.registerFrameTimerCallBack.set(id, callback)
}
removeFrameTimerCallback(id: string) {
this.registerFrameTimerCallBack.delete(id)
}
/**
* ID
* @param entityId
@ -169,17 +170,17 @@ export default class Viewport {
async initPhysics() {
// Physics variables
let collisionConfiguration;
let dispatcher;
let broadphase;
let solver;
collisionConfiguration = new this.ammoModel.btDefaultCollisionConfiguration();
dispatcher = new this.ammoModel.btCollisionDispatcher( collisionConfiguration );
broadphase = new this.ammoModel.btDbvtBroadphase();
solver = new this.ammoModel.btSequentialImpulseConstraintSolver();
this.physicsWorld = new this.ammoModel.btDiscreteDynamicsWorld( dispatcher, broadphase, solver, collisionConfiguration );
this.physicsWorld.setGravity( new this.ammoModel.btVector3( 0, 0, 0 ) );
let collisionConfiguration
let dispatcher
let broadphase
let solver
collisionConfiguration = new this.ammoModel.btDefaultCollisionConfiguration()
dispatcher = new this.ammoModel.btCollisionDispatcher(collisionConfiguration)
broadphase = new this.ammoModel.btDbvtBroadphase()
solver = new this.ammoModel.btSequentialImpulseConstraintSolver()
this.physicsWorld = new this.ammoModel.btDiscreteDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration)
this.physicsWorld.setGravity(new this.ammoModel.btVector3(0, 0, 0))
// this.transformAux1 = new this.ammoModel.btTransform();
// tempBtVec3_1 = new Ammo.btVector3( 0, 0, 0 );
@ -402,9 +403,9 @@ export default class Viewport {
}
offset = 0
clock = new THREE.Clock();
elapsedTime = 0;
interval = 1; // 触发间隔(秒)
clock = new THREE.Clock()
elapsedTime = 0
interval = 1 // 触发间隔(秒)
/**
*
*/
@ -418,18 +419,18 @@ export default class Viewport {
}
this.statsControls?.update()
const deltaTime = this.clock.getDelta();
const deltaTime = this.clock.getDelta()
this.updatePhysics(deltaTime)
this.renderer?.render(this.scene.scene, this.camera)
this.css2DRenderer.render(this.scene.scene, this.camera)
this.css3DRenderer.render(this.scene.scene, this.camera)
const delta = this.clock.getDelta();
this.elapsedTime += delta;
const delta = this.clock.getDelta()
this.elapsedTime += delta
if (this.elapsedTime >= this.interval) { // 每1秒触发一次
this.elapsedTime = 0;
this.elapsedTime = 0
this.registerFrameTimerCallBack.forEach(callback => {
if (typeof callback === 'function') {
callback()

26
src/core/manager/BackendMessageReceiver.ts

@ -42,11 +42,13 @@ export default class BackendMessageReceiver {
subscribedTopics: [] as string[]
})
// 启动MQTT连接
public async start(projectUuid: string, envId: number, config: MqttConfig): Promise<boolean> {
setProjectEnv(projectUuid: string, envId: number) {
this.projectUuid = projectUuid
this.envId = envId
}
// 启动MQTT连接
public async start(config: MqttConfig): Promise<boolean> {
// 如果已经连接,先断开
if (this.client?.connected) {
await this.dispose()
@ -69,14 +71,14 @@ export default class BackendMessageReceiver {
return new Promise((resolve) => {
try {
console.log('Connecting to MQTT broker:', config.websocket)
console.log('Connecting to backendMQTT broker:', config.websocket)
this.client = mqtt.connect(config.websocket, options)
// 连接成功
this.client.on('connect', () => {
this.state.status = ConnectionStatus.CONNECTED
this.state.isConnected = true
console.log('MQTT connected')
console.log('backendMQTT connected')
resolve(true)
})
@ -84,20 +86,20 @@ export default class BackendMessageReceiver {
this.client.on('close', () => {
this.state.status = ConnectionStatus.DISCONNECTED
this.state.isConnected = false
console.log('MQTT disconnected')
console.log('backendMQTT disconnected')
})
// 重连中
this.client.on('reconnect', () => {
this.state.status = ConnectionStatus.RECONNECTING
console.log('MQTT reconnecting...')
console.log('backendMQTT reconnecting...')
})
// 错误处理
this.client.on('error', (error) => {
this.state.status = ConnectionStatus.ERROR
this.state.lastError = error.message
console.error('MQTT error:', error)
console.error('backendMQTT error:', error)
resolve(false)
})
@ -109,7 +111,7 @@ export default class BackendMessageReceiver {
} catch (error) {
this.state.status = ConnectionStatus.ERROR
this.state.lastError = (error as Error).message
console.error('MQTT connection error:', error)
console.error('backendMQTT connection error:', error)
return false
}
})
@ -124,7 +126,7 @@ export default class BackendMessageReceiver {
this.state.isConnected = false
this.state.status = ConnectionStatus.DISCONNECTED
this.state.subscribedTopics = []
console.log('MQTT stopped')
console.log('backendMQTT stopped')
resolve()
})
} else {
@ -139,7 +141,7 @@ export default class BackendMessageReceiver {
const envId = this.envId
switch (type) {
case 'ServerState':
return [`/lcc/${projId}/${envId}/server`, this.handleServerState]
return [`/lcc/+/+/server`, this.handleServerState]
case 'ClientState':
return [`/lcc/${projId}/${envId}/client`, this.handleClientState]
case 'TaskUpdate':
@ -164,7 +166,7 @@ export default class BackendMessageReceiver {
// 订阅主题
public subscribe(type: BackendTopicType, handler: BackendMessageHandler): StopSubscribe {
if (!this.client?.connected) {
throw new Error('Cannot subscribe - MQTT not connected')
throw new Error('Cannot subscribe - backendMQTT not connected')
}
const [topic, processFn] = this.getTopicStringByType(type)
@ -196,7 +198,7 @@ export default class BackendMessageReceiver {
// 取消订阅
public unsubscribe(type: BackendTopicType, handler: BackendMessageHandler): void {
// if (!this.client?.connected) {
// throw new Error('Cannot unsubscribe - MQTT not connected')
// throw new Error('Cannot unsubscribe - backendMQTT not connected')
// }
const [topic, processFn] = this.getTopicStringByType(type)

35
src/core/manager/EnvManager.ts

@ -28,11 +28,7 @@ export default class EnvManager {
console.error('Error:', error)
}
async start(env: EnvInfo) {
if (!env) {
system.showErrorDialog('Environment is not specified, cannot start EnvManager.')
return
}
async connectEnv() {
if (!worldModel.state.isOpened) {
system.showErrorDialog('WorldModel is not opened, cannot start EnvManager.')
return
@ -45,26 +41,22 @@ export default class EnvManager {
system.showErrorDialog('EnvManager is already running, cannot start again.')
return
}
if (!env.envConfig.mqtt.websocket) {
system.showErrorDialog('MQTT websocket URL is not set in the envConfig.mqtt.')
if (!worldModel.state.runState.currentEnv) {
system.showErrorDialog('Environment is not specified, cannot start EnvManager.')
return
}
if (!env.envConfig.frontendMqtt.websocket) {
system.showErrorDialog('Frontend MQTT websocket URL is not set in the envConfig.frontendMqtt.')
if (!worldModel.state.runState.currentEnv.envConfig.mqtt.websocket) {
system.showErrorDialog('MQTT websocket URL is not set in the envConfig.mqtt.')
return
}
await this.stop()
await this.disconnectEnv()
system.showLoading()
worldModel.state.runState.isLoading = true
worldModel.state.runState.currentEnv = env
const env = worldModel.state.runState.currentEnv
try {
await LCC.serverStart()
await worldModel.backendMessageReceiver.start(
worldModel.state.project_uuid,
worldModel.state.runState.currentEnvId,
env.envConfig.frontendMqtt)
worldModel.backendMessageReceiver.setProjectEnv(worldModel.state.project_uuid, worldModel.state.runState.currentEnvId)
await LCC.loadInv()
this.client = mqtt.connect(env.envConfig.mqtt.websocket, {
@ -91,12 +83,9 @@ export default class EnvManager {
}
}
async stop() {
async disconnectEnv() {
system.showLoading()
try {
if (window['LCC']) {
await LCC.serverStop()
}
worldModel.state.runState.isRunning = false
if (this.client) {
this.client.removeAllListeners()
@ -126,10 +115,10 @@ export default class EnvManager {
/**
*
*/
static async getAllEnv(worldId: string): Promise<ServerResponse<EnvInfo[]>> {
static async getAllEnv(worldId: string): Promise<EnvInfo[]> {
// system.invokeServer('')
if (!worldId) {
return Promise.resolve({ success: true, data: [], msg: '' })
return Promise.reject('World ID is not provided.')
}
const res = await Request.request.post('/api/workbench/EnvController@getAllEnv', {
worldId: worldId
@ -142,6 +131,6 @@ export default class EnvManager {
*
*/
dispose(): void {
this.stop()
this.disconnectEnv()
}
}

55
src/core/manager/WorldModel.ts

@ -4,13 +4,13 @@ import { Request } from '@ease-forge/shared'
import EventBus from '@/runtime/EventBus'
import StateManager from '@/core/manager/StateManager.ts'
import { getQueryParams, setQueryParam } from '@/utils/webutils.ts'
import localforage from 'localforage'
import BackendMessageReceiver from '@/core/manager/BackendMessageReceiver.ts'
import EnvManager from '@/core/manager/EnvManager.ts'
import RCSScript from '@/core/script/RCSScript.ts'
import LCCScript from '@/core/script/LCCScript.ts'
export interface WorldModelState {
authorizationConfig: ServerAuthorizationConfigVo // 服务器授权配置
isOpened: boolean // 是否已打开世界模型
worldData: any // 世界模型数据, 包含排除 items 楼层数据之外的所有数据
@ -20,6 +20,7 @@ export interface WorldModelState {
server: string // 当前楼层服务器地址
project_uuid: string // 当前楼层所在项目ID
project_label: string // 项目名称
sub_system_list: string[] // 子系统集合, 用于标识当前楼层所属的子系统
catalogCode: string // 当前楼层的目录代码
stateManagerId: string // 当前楼层的状态管理器id
@ -47,6 +48,7 @@ export default class WorldModel {
*
*/
state: WorldModelState = reactive({
authorizationConfig: null,
isOpened: false, // 是否已打开世界模型
worldData: null, // 世界模型数据, 包含排除 items 楼层数据之外的所有数据
@ -56,11 +58,13 @@ export default class WorldModel {
server: '',
project_uuid: '', // 项目ID
project_label: '', // 项目名称
sub_system_list: [], // 子系统集合, 用于标识当前楼层所属的子系统
catalogCode: '', // 当前楼层的目录代码
stateManagerId: '', // 当前楼层的状态管理器id, 一般是 项目ID+目录项ID
isDraft: false, // 是否是草稿数据, 如果是草稿数据, 则不需要再从服务器加载数据
runState: {
currentEnvId: null,
isLoading: false,
@ -91,13 +95,19 @@ export default class WorldModel {
})
}
constructor() {
}
/**
*
*/
init() {
async init() {
system.showLoading('Authenticating...')
const configRes = await LCC.getAuthorizationConfig()
if (!configRes.success) {
system.showErrorDialog('Authentication failed: ' + configRes.msg)
return Promise.reject(configRes)
}
this.state.authorizationConfig = configRes.data
await this.backendMessageReceiver.start(configRes.data.frontendMqtt)
// 观察 this.state.catalogCode 的变化, 如果变化就调用 catalogCodeChange 方法
return Promise.all([
import('../../modules/measure'),
@ -112,8 +122,9 @@ export default class WorldModel {
import('../../modules/amr/ptr/clx'),
import('../../modules/charger')
]).then(() => {
]).then((configRes) => {
console.log('世界模型初始化完成')
system.clearLoading()
// 尝试从草稿中加载数据
const stateManagerId = getQueryParams()?.get('store')
@ -128,6 +139,8 @@ export default class WorldModel {
this.state.catalog = data.catalog
this.state.server = data.server
this.state.project_uuid = data.project_uuid
this.state.project_label = data.project_label
this.state.sub_system_list = data.sub_system_list || []
this.state.isDraft = true
this.tryOpenCatelog(data.catalogCode, true).then(() => {
@ -186,6 +199,7 @@ export default class WorldModel {
this.state.server = lccModelWorld.server
this.state.project_uuid = lccModelWorld.projectUuid
this.state.project_label = lccModelWorld.projectLabel
this.state.sub_system_list = lccModelWorld.subSystemList || []
// 没有打开楼层,不加载 this.state.catalogCode
this.state.isDraft = false
@ -207,6 +221,8 @@ export default class WorldModel {
catalog: _.cloneDeep(this.state.catalog),
server: this.state.server,
project_uuid: this.state.project_uuid,
project_label: this.state.project_label,
sub_system_list: _.cloneDeep(this.state.sub_system_list),
catalogCode: catalogCode,
worldData: _.cloneDeep(this.state.worldData)
}
@ -222,6 +238,33 @@ export default class WorldModel {
stateManager: null
})
}
/**
*
*/
setEnv(env: EnvInfo) {
if (this.state.runState.currentEnvId && this.state.runState.isRunning) {
system.showErrorDialog('cannot change env when running')
} else {
// 更新当前环境 ID
if (!env) {
this.state.runState.currentEnvId = null
this.state.runState.isLoading = false
this.state.runState.isRunning = false
this.state.runState.isVirtual = false
this.state.runState.timeRate = 0
this.state.runState.currentEnv = null
} else {
this.state.runState.currentEnvId = env.envId
this.state.runState.isLoading = false
this.state.runState.isRunning = false
this.state.runState.isVirtual = env.isVirtual
this.state.runState.timeRate = 1
this.state.runState.currentEnv = env
}
}
}
}
const worldModel = new WorldModel()

84
src/core/script/LCCScript.ts

@ -6,49 +6,53 @@ import { Request } from '@ease-forge/shared'
* LCC API
*/
export default class LCCScript implements LCC {
/**
*
* @param option projectUUID envId
*/
queryServerState(option: { projectUUID?: string; envId?: string } = {}): Promise<ServerResponse<ServerStatusVo[]>> {
return Request.request.post('/api/workbench/ServerController@queryServerState', {
_id: system.createUUID(),
...option
})
}
log(from: string, message: string, ...args: any[]): void {
// 插入时分秒 HH:mm:ss.sss
const now = new Date().toISOString().replace('T', ' ').replace('Z', '').split(' ')[1]
// 打成彩色
// console.log(now + ' [LCC-' + from + '] ' + message, ...args)
console.log(`%c${now} [${from}] ${message}`, 'color: #00f', ...args)
startServer(projectUUID: string, envId: string): Promise<ServerResponse<String>> {
return Request.request.post('/api/workbench/ServerController@startServer', {
projectUUID: projectUUID,
envId: envId
})
}
/**
* - 使
*/
async sleep(timeOfMs: number = 1000): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
resolve() // 确保调用 resolve
}, timeOfMs)
stopServer(projectUUID: string, envId: string): Promise<ServerResponse<String>> {
return Request.request.post('/api/workbench/ServerController@stopServer', {
projectUUID: projectUUID,
envId: envId
})
}
async getAllProjects(): Promise<ServerResponse<LccProjectVo[]>> {
return Request.request.post('/api/workbench/LccController@getAllProjects', {})
connectServer(): Promise<void> {
throw new Error('Method not implemented. Please use the connectServer method from the worldModel.')
}
async serverStart(): Promise<ServerResponse<boolean>> {
if (!worldModel.state.project_uuid || !worldModel.state.runState.currentEnvId) {
return Promise.reject(new Error('Project UUID or Environment ID is not set.'))
}
disconnectServer(): Promise<void> {
throw new Error('Method not implemented. Please use the disconnectServer method from the worldModel.')
}
return Request.request.post('/api/workbench/LccController@serverStart', {
projectUUID: worldModel.state.project_uuid,
envId: worldModel.state.runState.currentEnvId
getAuthorizationConfig(): Promise<ServerResponse<ServerAuthorizationConfigVo>> {
return Request.request.post('/api/workbench/AuthController@getAuthorizationConfig', {
_id: system.createUUID()
})
}
async serverStop(): Promise<ServerResponse<boolean>> {
if (!worldModel.state.project_uuid || !worldModel.state.runState.currentEnvId) {
return Promise.reject(new Error('Project UUID or Environment ID is not set.'))
}
return Request.request.post('/api/workbench/LccController@serverStop', {
projectUUID: worldModel.state.project_uuid,
envId: worldModel.state.runState.currentEnvId
/**
* - 使
*/
async sleep(timeOfMs: number = 1000): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
resolve() // 确保调用 resolve
}, timeOfMs)
})
}
@ -140,11 +144,17 @@ export default class LCCScript implements LCC {
return res.data
}
subscribe(topicType: BackendTopicType, eventHandler: BackendMessageHandler) {
return worldModel.backendMessageReceiver.subscribe(topicType, eventHandler)
}
unsubscribe(topicType: BackendTopicType, eventHandler: BackendMessageHandler) {
worldModel.backendMessageReceiver.unsubscribe(topicType, eventHandler)
/**
*
* @param from Worker
* @param message
* @param args
*/
log(from: string, message: string, ...args: any[]): void {
// 插入时分秒 HH:mm:ss.sss
const now = new Date().toISOString().replace('T', ' ').replace('Z', '').split(' ')[1]
// 打成彩色
// console.log(now + ' [LCC-' + from + '] ' + message, ...args)
console.log(`%c${now} [${from}] ${message}`, 'color: #00f', ...args)
}
}

6
src/editor/ModelMain.less

@ -18,6 +18,12 @@
margin: 0 20px;
}
.app-header-text {
color: #f4c521;
font-size: large;
line-height: 50px;
}
.app-header-menu-wrap {
flex: 1;
display: flex;

110
src/editor/ModelMain.vue

@ -9,38 +9,10 @@
<component :is="renderIcon('element ArrowDown')"></component>
</div>
<div style="flex-grow: 1;">
<span>{{ worldModelState.project_label }}</span>
<span class="app-header-text">{{ worldModelState.project_label }}</span>
</div>
<div v-if="isModelOpen" style="display: flex; flex-direction: row; align-items: center; margin-right: 10px;">
<div class="field-block" style="margin-right: 5px;">
<el-select style="width:180px;" placeholder="选择运行环境" :disabled="worldModelState.runState.currentEnvId && worldModelState.runState.isRunning"
v-model="currentEnvId">
<el-option v-for="env in envList" :key="env.envId" :label="env.envName" :value="env.envId"></el-option>
<template #footer>
<el-button size="small" type="primary" @click="createEnv" plain>创建虚拟环境</el-button>
<el-button size="small" :icon="renderIcon('Refresh')" @click="reloadEnvList" />
</template>
</el-select>
</div>
<div class="field-block" style="margin-right: 5px;">
<el-select style="width:80px;" placeholder="时间速率"
v-model="worldModelState.runState.timeRate"
v-if="worldModelState.runState.isVirtual">
<el-option v-for="option in timeRateOptions"
:label="option.label" :value="option.value" />
</el-select>
</div>
<el-button :icon="renderIcon('Play')" type="primary"
v-if="!worldModelState.runState.currentEnvId || !worldModelState.runState.isRunning"
:disabled="!worldModelState.runState.currentEnvId"
:loading="worldModelState.runState.isLoading"
@click="startEnv">启动服务
</el-button>
<el-button :icon="renderIcon('Stop')" type="danger" plain
v-if="worldModelState.runState.currentEnvId && worldModelState.runState.isRunning"
:loading="worldModelState.runState.isLoading"
@click="stopEnv">停止服务
</el-button>
<EnvSelectConnect />
</div>
</div>
<div class="user">
@ -155,10 +127,10 @@ import Logo from '@/assets/images/logo.png'
import './ModelMain.less'
import EventBus from '@/runtime/EventBus.js'
import { worldModel } from '@/core/manager/WorldModel.ts'
import EnvManager from '@/core/manager/EnvManager.js'
import EnvSelectConnect from '@/editor/widgets/server/EnvSelectConnect.vue'
export default {
components: { Model2DEditor, Model3DViewer, Split, SplitArea, CatalogDefine },
components: { Model2DEditor, Model3DViewer, Split, SplitArea, CatalogDefine, EnvSelectConnect },
created() {
ModelMainInit()
},
@ -193,7 +165,7 @@ export default {
}
})
})
this.reloadEnvList()
EventBus.on('dataLoadComplete', (data) => {
const { stateManager } = data
if (stateManager) {
@ -210,13 +182,6 @@ export default {
data() {
return {
Logo,
timeRateOptions: [
{ label: '1x', value: 1 },
{ label: '2x', value: 2 },
{ label: '5x', value: 5 },
{ label: '10x', value: 10 }
],
envList: [],
isShowEditor: false,
editorHash: 0,
currentViewport: null,
@ -231,8 +196,7 @@ export default {
sectionRightName: 'property',
sectionBottomName: '',
sectionLeftSearch: '',
centerActiveName: 'ModelEditor',
currentEnvId: worldModel.state.runState.currentEnvId
centerActiveName: 'ModelEditor'
}
},
computed: {
@ -272,48 +236,6 @@ export default {
}
},
watch: {
'worldModelState.isOpened': {
handler() {
if (this.worldModelState.isOpened) {
this.reloadEnvList()
}
}
},
'worldModelState.project_uuid': {
immediate: true,
handler() {
if (this.worldModelState.isOpened) {
this.reloadEnvList()
}
}
},
'currentEnvId': {
handler(newVal, originalVal) {
if (this.worldModelState.runState.currentEnvId && this.worldModelState.runState.isRunning) {
throw new Error('cannot change env when running')
this.currentEnvId = originalVal
} else {
// ID
const env = _.find(this.envList, env => env.envId === newVal)
if (!env) {
this.worldModelState.runState.currentEnvId = newVal
this.worldModelState.runState.isLoading = false
this.worldModelState.runState.isRunning = false
this.worldModelState.runState.isVirtual = false
this.worldModelState.runState.timeRate = 0
this.worldModelState.runState.currentEnv = null
} else {
this.worldModelState.runState.currentEnvId = newVal
this.worldModelState.runState.isLoading = false
this.worldModelState.runState.isRunning = false
this.worldModelState.runState.isVirtual = env.isVirtual
this.worldModelState.runState.timeRate = 1
this.worldModelState.runState.currentEnv = env
}
}
}
},
hideBottom(value) {
if (value) {
this.$refs.mainSplit.refreshSize([100, 0])
@ -331,26 +253,6 @@ export default {
methods: {
renderIcon,
getWidgetBySide,
startEnv() {
const env = this.envList.find(env => env.envId === this.worldModelState.runState.currentEnvId)
if (env) {
worldModel.envManager.start(env)
}
},
stopEnv() {
worldModel.envManager.stop()
},
createEnv() {
EnvManager.createEnv(this.worldModelState.project_uuid).then(() => {
this.reloadEnvList()
})
},
reloadEnvList() {
EnvManager.getAllEnv(this.worldModelState.project_uuid)
.then(envList => {
this.envList = envList
})
},
toHome() {
system.router.push({ name: 'home' })
},

14
src/editor/menus/FileMenu.ts

@ -55,7 +55,7 @@ function addProject(successful?: Function) {
export default defineMenu((menus) => {
menus.insertChildren('file',
{
name: 'file', label: '模型', icon: renderIcon('ModelFile'), order: 1, disabled: false
name: 'file', label: '地图模型', icon: renderIcon('ModelFile'), order: 1, disabled: false
},
[
{
@ -101,13 +101,12 @@ export default defineMenu((menus) => {
{
name: 'save', label: '保存', icon: SvgCode.save, order: 2, tip: 'Ctrl+S',
click: async () => {
if (!worldModel.state.runState.currentEnvId) {
system.showErrorDialog('请先选择环境')
return
}
system.showLoading('正在保存模型数据...')
const viewport: Viewport = window['viewport']
if (!viewport) {
system.showErrorDialog('not found viewport')
return
}
const vdata: any = await viewport.stateManager.save()
for (const item of vdata.items) {
@ -128,9 +127,10 @@ export default defineMenu((menus) => {
await Request.request.post('/api/workbench/LccModelManager@addOrUpdateWorld', {
projectUuid: worldModel.state.project_uuid,
projectLabel: worldModel.state.project_label,
subSystemList: worldModel.state.sub_system_list,
directoryData: JSON.stringify(worldModel.state.catalog),
envId: worldModel.state.runState.currentEnvId,
otherData: JSON.stringify(worldModel.state.worldData)
otherData: JSON.stringify(worldModel.state.worldData.otherData)
})
system.msg('保存成功', 'success')

5
src/editor/widgets/IWidgets.ts

@ -32,8 +32,11 @@ export default defineComponent({
if (!worldModel.state.isOpened) {
return '地图未打开'
}
if (!worldModel.state.runState.currentEnvId) {
return '环境未选择'
}
if (!worldModel.backendMessageReceiver.state.isConnected) {
return '后端连接异常'
return '项目未启动'
}
return ''
}

6
src/editor/widgets/monitor/MonitorView.vue

@ -11,7 +11,7 @@
</span>
</div>
<div class="calc-left-panel">
<el-empty v-if="!isActivated || errorDescription" :description="errorDescription">
<el-empty v-if="!isActivated || errorDescription" :description="errorDescription" style="width:100%;">
</el-empty>
<div v-else class="monitor-tool-wrap">
<div class="infor-row">
@ -150,10 +150,10 @@ export default {
//
this.stopSubscribe.push(
LCC.subscribe('DeviceAlive', this.onDeviceAliveMessage.bind(this))
worldModel.backendMessageReceiver.subscribe('DeviceAlive', this.onDeviceAliveMessage.bind(this))
)
this.stopSubscribe.push(
LCC.subscribe('DeviceStatus', this.onDeviceStatusMessage.bind(this))
worldModel.backendMessageReceiver.subscribe('DeviceStatus', this.onDeviceStatusMessage.bind(this))
)
},
undescribe() {

8
src/editor/widgets/script/ScriptMeta.ts

@ -1,12 +1,12 @@
import { defineWidget } from '../../../runtime/DefineWidget.ts'
import { defineWidget } from '@/runtime/DefineWidget.ts'
import { renderIcon } from '@/utils/webutils.ts'
import ScriptView from './ScriptView.vue'
export default defineWidget({
name: 'script',
title: '脚本',
icon: renderIcon('antd CodeOutlined'),
title: '自定义脚本',
icon: renderIcon('fa Code'),
side: 'bottom',
order: 3,
component: ScriptView
})
})

108
src/editor/widgets/server/EnvSelectConnect.vue

@ -0,0 +1,108 @@
<template>
<el-select placeholder="选择运行环境" style="width:180px; margin-right: 5px;"
:disabled="worldModelState.runState.currentEnvId && worldModelState.runState.isRunning"
:model-value="worldModelState.runState.currentEnvId"
@change="setEnvId">
<el-option v-for="env in envList" :key="env.envId" :label="env.envName" :value="env.envId"></el-option>
<template #footer>
<el-button size="small" type="primary" @click="createEnv" plain>创建虚拟环境</el-button>
<el-button size="small" :icon="renderIcon('Refresh')" @click="reloadEnvList" />
</template>
</el-select>
<el-select style="width:80px;margin-right: 5px;" placeholder="时间速率"
v-model="worldModelState.runState.timeRate"
v-if="worldModelState.runState.isVirtual">
<el-option v-for="option in timeRateOptions"
:label="option.label" :value="option.value" />
</el-select>
<el-button :icon="renderIcon('Connection')" type="primary"
v-if="!worldModelState.runState.currentEnvId || !worldModelState.runState.isRunning"
:disabled="!worldModelState.runState.currentEnvId"
:loading="worldModelState.runState.isLoading"
@click="connectEnv">连接服务
</el-button>
<el-button :icon="renderIcon('antd DisconnectOutlined')" type="danger" plain
v-if="worldModelState.runState.currentEnvId && worldModelState.runState.isRunning"
:loading="worldModelState.runState.isLoading"
@click="disconnectEnv">断开连接
</el-button>
</template>
<script>
import { renderIcon } from '@/utils/webutils.js'
import { worldModel } from '@/core/manager/WorldModel.js'
import EnvManager from '@/core/manager/EnvManager.js'
import _ from 'lodash'
export default {
name: 'EnvSelectConnect',
data() {
return {
/**
* @type {Array<EnvInfo>}
*/
envList: [],
timeRateOptions: [
{ label: '1x', value: 1 },
{ label: '2x', value: 2 },
{ label: '5x', value: 5 },
{ label: '10x', value: 10 }
]
}
},
mounted() {
this.reloadEnvList()
},
methods: {
renderIcon,
connectEnv() {
worldModel.envManager.connectEnv()
},
disconnectEnv() {
worldModel.envManager.disconnectEnv()
},
createEnv() {
EnvManager.createEnv(this.worldModelState.project_uuid).then(() => {
this.reloadEnvList()
})
},
reloadEnvList() {
EnvManager.getAllEnv(this.worldModelState.project_uuid)
.then(envList => {
this.envList = envList
})
},
setEnvId(newVal) {
const env = _.find(this.envList, env => env.envId === newVal)
if (!env) {
system.showErrorDialog(`未找到环境ID: ${newVal}`)
}
worldModel.setEnv(env)
}
},
watch: {
'worldModelState.isOpened': {
handler() {
if (this.worldModelState.isOpened) {
this.reloadEnvList()
}
}
},
'worldModelState.project_uuid': {
immediate: true,
handler() {
if (this.worldModelState.isOpened) {
this.reloadEnvList()
}
}
}
},
computed: {
isModelOpen() {
return this.worldModelState.isOpened
},
worldModelState() {
return worldModel.state
}
}
}
</script>

2
src/editor/widgets/server/ServerMeta.ts

@ -5,7 +5,7 @@ import ServerView from './ServerView.vue'
export default defineWidget({
name: 'server',
title: '服务管理',
icon: renderIcon('Server'),
icon: renderIcon('antd DatabaseTwotone'),
side: 'bottom',
order: 3,
component: ServerView

162
src/editor/widgets/server/ServerView.vue

@ -6,14 +6,14 @@
<component :is="renderIcon('element Search')"></component>
</template>
</el-input>
<el-button :icon="renderIcon('Refresh')" size="small">刷新</el-button>
<el-button :icon="renderIcon('Refresh')" size="small" @click="refreshData">刷新</el-button>
<span class="close" @click="closeMe">
<component :is="renderIcon('element Close')"></component>
</span>
</div>
<div class="calc-bottom-panel">
<el-table :data="serverList" style="width: 100%">
<el-table-column prop="envID" label="环境ID" width="100" />
<el-table :data="serverList" v-loading="isLoading" style="width: 100%">
<el-table-column prop="envId" label="环境ID" width="100" />
<el-table-column prop="isVirtual" label="环境类型" width="100">
<template #default="{ row }">
<el-tag :type="row.isVirtual ? 'info' : 'primary'">
@ -26,17 +26,17 @@
<el-table-column label="系统类型" min-width="200">
<template #default="{ row }">
<div class="system-tags">
<el-tag v-for="(system, index) in row.systemList" :key="index" size="small" class="system-tag" :class="getSystemTagClass(system)">
<el-tag v-for="(system, index) in row.subSystemList" :key="index" size="small" class="system-tag" :class="getSystemTagClass(system)">
{{ system }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="projectIsRunning" label="运行状态" width="100">
<el-table-column prop="isRunning" label="运行状态" width="100">
<template #default="{ row }">
<div class="status-indicator">
<span class="dot" :class="row.projectIsRunning ? 'running' : 'stopped'"></span>
{{ row.projectIsRunning ? '运行中' : '已停止' }}
<span class="dot" :class="row.isRunning ? 'running' : 'stopped'"></span>
{{ row.isRunning ? '运行中' : '已停止' }}
</div>
</template>
</el-table-column>
@ -52,15 +52,11 @@
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button :type="row.projectIsRunning ? 'danger' : 'success'" plain size="small" @click="toggleServerStatus(row)">
{{ row.projectIsRunning ? '停止' : '启动' }}
<el-button v-if="!row.isRunning" type="success" plain size="small" @click="startServer(row)"
:icon="renderIcon('Play')">启动
</el-button>
</template>
</el-table-column>
<el-table-column label="连接" width="100">
<template #default="{ row }">
<el-button type="primary" plain size="small" @click="connectEnv(row)">
连接
<el-button v-if="row.isRunning" type="danger" plain size="small" @click="stopServer(row)"
:icon="renderIcon('Stop')">停止
</el-button>
</template>
</el-table-column>
@ -69,53 +65,72 @@
</template>
<script>
import IWidgets from '../IWidgets.js'
import { worldModel } from '@/core/manager/WorldModel.js'
export default {
name: 'ServerView',
mixins: [IWidgets],
data() {
return {
isLoading: false,
stopSubscribe: [],
// - API
searchKeyword: '',
serverList: [
{
id: 1,
isVirtual: true,
projectUuid: 'PROJ-001',
envID: '101',
projectLabel: '物流系统A',
systemList: ['WMS', 'WCS', 'RCS'],
projectIsRunning: true,
startTime: Date.now() - 3600000 * 2.5 // 2.5
},
{
id: 2,
isVirtual: false,
projectUuid: 'PROJ-002',
envID: '102',
projectLabel: '仓储系统B',
systemList: ['MFC', 'WES'],
projectIsRunning: false,
startTime: null
},
{
id: 3,
isVirtual: true,
projectUuid: 'PROJ-003',
envID: '103',
projectLabel: '配送系统C',
systemList: ['OES', 'PES', 'WMS', 'WCS'],
projectIsRunning: true,
startTime: Date.now() - 3600000 * 4.2 // 4.2
}
]
/**
* @type {Array<ServerStatusVo>}
*/
serverList: []
}
},
mounted() {
window['ServerView'] = this
},
beforeUnmount() {
unmounted() {
window['ServerView'] = null
this.undescribe()
},
methods: {
/**
* @type {ServerStateFn}
*/
onServerStateMessage(type, topic, data) {
//
const server = this.serverList.find(s => s.envId === data.envId && s.projectUuid === data.projectUuid)
if (server) {
Object.assign(server, data)
}
},
async subscribe() {
await this.refreshData()
//
this.stopSubscribe.push(
worldModel.backendMessageReceiver.subscribe('ServerState', this.onServerStateMessage.bind(this))
)
},
async refreshData() {
this.serverList = []
this.isLoading = true
try {
const res = await LCC.queryServerState()
if (!res.success) {
return
}
this.serverList = res.data
} finally {
this.isLoading = false
}
},
undescribe() {
//
for (const stopFn of this.stopSubscribe) {
stopFn()
}
this.stopSubscribe = []
},
//
getSystemTagClass(system) {
return `system-${system.toLowerCase()}`
@ -123,50 +138,49 @@ export default {
//
formatStartTime(row) {
if (!row.projectIsRunning || !row.startTime) return ''
if (!row.startTime) return ''
const date = new Date(row.startTime)
const date = new Date(parseInt(row.startTime))
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}
${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`
},
//
calculateRuntime(row) {
if (!row.projectIsRunning || !row.startTime) return ''
if (!row.startTime) return ''
const hours = (Date.now() - row.startTime) / 3600000
return hours.toFixed(1)
},
//
toggleServerStatus(row) {
if (row.projectIsRunning) {
this.stopServer(row)
} else {
this.startServer(row)
}
},
//
startServer(server) {
server.projectIsRunning = true
server.startTime = Date.now()
// API
console.log('启动服务器:', server.id)
system.showLoading()
LCC.startServer(server.projectUuid, server.envId)
.finally(() => {
system.clearLoading()
})
},
//
stopServer(server) {
server.projectIsRunning = false
// API
console.log('停止服务器:', server.id)
},
//
connectEnv(row) {
console.log('连接环境:', row.envID)
//
this.$message.success(`正在连接环境: ${row.envID}`)
system.showLoading()
LCC.stopServer(server.projectUuid, server.envId)
.finally(() => {
system.clearLoading()
})
}
},
watch: {
isActivated: {
handler(value) {
if (value) {
this.subscribe()
} else {
this.undescribe()
}
},
immediate: true
}
}
}

6
src/editor/widgets/task/TaskMeta.ts

@ -4,9 +4,9 @@ import TaskView from './TaskView2.vue'
export default defineWidget({
name: 'task',
title: '任务',
icon: renderIcon('element List'),
title: 'AGV任务',
icon: renderIcon('antd CarryOutOutlined'),
side: 'bottom',
order: 1,
component: TaskView
})
})

12
src/editor/widgets/toolbox/ToolboxView.vue

@ -102,8 +102,8 @@ export default {
{
name: 'other', icon: 'antd CiOutlined', label: '辅助',
children: [
{ name: 'source', icon:'antd SoundOutlined',label: '发生器' },
{ name: 'sink',icon:'fa Eraser', label: '消失器' },
{ name: 'source', icon: 'antd SoundOutlined', label: '发生器' },
{ name: 'sink', icon: 'fa Eraser', label: '消失器' },
{ name: 'dispatcher', label: '任务分配器' },
{ name: 'text', label: '文本' },
{ name: 'image', label: '图片' },
@ -157,6 +157,12 @@ export default {
</script>
<style lang="less">
.toolbox-view {
width: 100%;
& > .el-menu {
width: 100%;
}
.subtitle {
margin-left: 5px;
color: var(--el-color-info-light-5);
@ -170,4 +176,4 @@ export default {
height: 35px;
}
}
</style>
</style>

100
src/types/LCC.d.ts

@ -14,54 +14,55 @@ declare interface LCC {
sleep(timeOfMs: number = 1000): Promise<void>
/**
*
* , Model
*/
getAllProjects(): Promise<ServerResponse<LccProjectVo[]>>
loadInv(): Promise<ServerResponse<InvVo>>
/**
*
* Model
*/
serverStart(): Promise<ServerResponse<boolean>>
loadExecutor(): Promise<ExecutorVo>
/**
*
*
* @param scriptList
*/
serverStop(): Promise<ServerResponse<boolean>>
saveAndSyncScripts(scriptList: { name: string, content: string }[]): Promise<ServerResponse<{ name: string, content: string }[]>>
/**
* , Model
*
*/
loadInv(): Promise<ServerResponse<InvVo>>
queryDeviceInfoList(): Promise<ServerResponse<DeviceVo[]>>
/**
* Model
*
*/
loadExecutor(): Promise<ExecutorVo>
queryServerState(option: { projectUUID?: string, envId?: string } = {}): Promise<ServerResponse<ServerStatusVo[]>>
/**
*
* @param scriptList
*
*/
saveAndSyncScripts(scriptList: { name: string, content: string }[]): Promise<ServerResponse<{ name: string, content: string }[]>>
startServer(projectUUID: string, envId: string): Promise<ServerResponse<String>>
/**
*
* @param topicType
* @param eventHandler
*
*/
subscribe(topicType: BackendTopicType, eventHandler: BackendMessageHandler): StopSubscribe;
stopServer(projectUUID: string, envId: string): Promise<ServerResponse<String>>
/**
*
* @param topicType
* @param eventHandler
* , worldModel
*/
unsubscribe(topicType: BackendTopicType, eventHandler: BackendMessageHandler);
connectServer(): Promise<void>
/**
*
*
*/
queryDeviceInfoList(): Promise<ServerResponse<DeviceVo[]>>
disconnectServer(): Promise<void>
/**
*
*/
getAuthorizationConfig(): Promise<ServerResponse<ServerAuthorizationConfigVo>>
}
/**
@ -74,8 +75,30 @@ type DeviceAliveFn = (type: BackendTopicType, topic: string, body: DeviceAliveVo
type DeviceStatusFn = (type: BackendTopicType, topic: string, body: DeviceVo) => void
type ServerStateFn = (type: BackendTopicType, topic: string, body: ServerStatusVo) => void
type StopSubscribe = () => void
interface ServerAuthorizationConfigVo {
/**
*
*/
authorizationCode: string
/**
*
*/
expirationTime: number
/**
*
*/
frontendMqtt: {
brokerUrl: string
username: string
password: string
websocket: string
}
}
/**
*
*/
@ -144,6 +167,37 @@ interface DeviceVo {
bizLpn: string
}
/**
*
*/
interface ServerStatusVo {
projectUuid: string;
envId: number;
isVirtual: boolean;
serverId: string;
isRunning: boolean;
startTime: number;
stopTime: number;
timeRate: number;
subSystemList: string[];
cpuUsage: number;
memoryUsage: number;
diskIoLoad: number;
/**
* GB
*/
freeMemory: number;
/**
* GB
*/
diskFreeSpace: number;
envConfig: EnvConfigVo;
projectLabel: string;
}
interface InvVo {
lpn: string
container_type: ContainerT

1
src/types/Model.d.ts

@ -220,7 +220,6 @@ interface EnvInfo {
interface EnvConfigVo {
mqtt: MqttConfig;
frontendMqtt: MqttConfig;
}
interface MqttConfig {

10
src/types/Types.d.ts

@ -360,6 +360,16 @@ interface VData {
project_uuid?: string
/**
*
*/
project_label?: string
/**
*
*/
sub_system_list?: string[]
/**
*
*/
catalogCode: string

Loading…
Cancel
Save