remove cocos-extensions
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +1,6 @@
|
||||
[submodule "thirdparty/BehaviourTree-ai"]
|
||||
path = thirdparty/BehaviourTree-ai
|
||||
url = https://github.com/esengine/BehaviourTree-ai.git
|
||||
[submodule "thirdparty/admin-backend"]
|
||||
path = thirdparty/admin-backend
|
||||
url = https://github.com/esengine/admin-backend.git
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"codeGeneration": {
|
||||
"template": "typescript",
|
||||
"useStrictMode": true,
|
||||
"generateComments": true,
|
||||
"generateImports": true,
|
||||
"componentSuffix": "Component",
|
||||
"systemSuffix": "System",
|
||||
"indentStyle": "spaces",
|
||||
"indentSize": 4
|
||||
},
|
||||
"performance": {
|
||||
"enableMonitoring": true,
|
||||
"warningThreshold": 16.67,
|
||||
"criticalThreshold": 33.33,
|
||||
"memoryWarningMB": 100,
|
||||
"memoryCriticalMB": 200,
|
||||
"maxRecentSamples": 60,
|
||||
"enableFpsMonitoring": true,
|
||||
"targetFps": 120
|
||||
},
|
||||
"debugging": {
|
||||
"enableDebugMode": true,
|
||||
"showEntityCount": true,
|
||||
"showSystemExecutionTime": true,
|
||||
"enablePerformanceWarnings": true,
|
||||
"logLevel": "info",
|
||||
"enableDetailedLogs": false
|
||||
},
|
||||
"editor": {
|
||||
"autoRefreshAssets": true,
|
||||
"showWelcomePanelOnStartup": true,
|
||||
"enableAutoUpdates": false,
|
||||
"updateChannel": "stable",
|
||||
"enableNotifications": true
|
||||
},
|
||||
"template": {
|
||||
"defaultEntityName": "ModifiedEntity",
|
||||
"defaultComponentName": "TestComponent",
|
||||
"defaultSystemName": "TestSystem",
|
||||
"createExampleFiles": true,
|
||||
"includeDocumentation": true,
|
||||
"useFactoryPattern": true
|
||||
},
|
||||
"events": {
|
||||
"enableEventSystem": true,
|
||||
"defaultEventPriority": 0,
|
||||
"enableAsyncEvents": true,
|
||||
"enableEventBatching": false,
|
||||
"batchSize": 10,
|
||||
"batchDelay": 16,
|
||||
"maxEventListeners": 100
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"description": "面板数据 / Panel data",
|
||||
"additionalProperties": false,
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9_-]+$": {
|
||||
"type": "object",
|
||||
"description": "面板名 / Panel name",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"default": "Default Panel",
|
||||
"description": "面板标题,支持 i18n:key / Panel title, support for i18n:key (required)"
|
||||
},
|
||||
"main": {
|
||||
"type": "string",
|
||||
"default": "dist/panels/default/index.js",
|
||||
"description": "入口函数 / Entry function (required)"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "面板图标存放相对目录 / Relative directory for panel icon storage"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["dockable", "simple"],
|
||||
"default": "dockable",
|
||||
"description": "面板类型(dockable | simple) / Panel type (dockable | simple)"
|
||||
},
|
||||
"flags": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resizable": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "是否可以改变大小,默认 true / Whether the size can be changed, default true"
|
||||
},
|
||||
"save": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "是否需要保存,默认 false / Whether to save, default false"
|
||||
},
|
||||
"alwaysOnTop": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "是否保持顶层显示,默认 false / Whether to keep the top level display, default false"
|
||||
}
|
||||
}
|
||||
},
|
||||
"size": {
|
||||
"type": "object",
|
||||
"description": "面板大小信息 / Panel size information",
|
||||
"properties": {
|
||||
"min-width": {
|
||||
"type": "number",
|
||||
"default": 200,
|
||||
"description": "面板最小宽度 / Minimum panel width"
|
||||
},
|
||||
"min-height": {
|
||||
"type": "number",
|
||||
"default": 200,
|
||||
"description": "面板最小高度 / Minimum panel height"
|
||||
},
|
||||
"width": {
|
||||
"type": "number",
|
||||
"default": 400,
|
||||
"description": " 面板默认宽度 / Panel Default Width"
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"default": 600,
|
||||
"description": "面板默认高度 / Panel Default Height"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["title", "main"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"description": "其他扩展插件的扩展配置 / Extended configuration for other extension plugins",
|
||||
"properties": {
|
||||
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"description": "插件定义文件 / Extension definition file",
|
||||
"properties": {
|
||||
"author": {
|
||||
"type": "string",
|
||||
"description": "作者 / Author",
|
||||
"default": "Cocos Creator Developer"
|
||||
},
|
||||
"contributions": {
|
||||
"$ref": "./contributions/index.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "object",
|
||||
"description": "发布时所需的依赖库 / Dependencies required for publishing"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "简要介绍扩展关键特性、用途,支持 i18n / Brief introduction of the key features and uses of the extension, supporting i18n"
|
||||
},
|
||||
"devDependencies": {
|
||||
"type": "object",
|
||||
"description": "开发时所需的依赖库 / Dependencies required for development"
|
||||
},
|
||||
"editor": {
|
||||
"type": "string",
|
||||
"description": "支持的 Cocos Creator 编辑器版本,支持 semver 格式 / Supported Cocos Creator editor version, supporting semver format"
|
||||
},
|
||||
"main": {
|
||||
"type": "string",
|
||||
"description": "入口函数 / Entry function",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "不能以 _ 或 . 开头、不能含有大写字母,也不能含有 URL 的非法字符例如 .、' 和 ,。 / Cannot start with _ or., cannot contain uppercase letters, and cannot contain URL illegal characters such as.,'and,",
|
||||
"default": "Custom Extension"
|
||||
},
|
||||
"package_version": {
|
||||
"type": "number",
|
||||
"description": "扩展系统预留版本号 / Extension system reserved version number",
|
||||
"default": 2
|
||||
},
|
||||
"panels": {
|
||||
"$ref": "./base/panels.json"
|
||||
},
|
||||
"scripts": {
|
||||
"type": "object",
|
||||
"description": "NPM 脚本 / NPM scripts"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "版本号字符串 / Version number string",
|
||||
"default": "1.0.0"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"author",
|
||||
"name",
|
||||
"package_version",
|
||||
"version"
|
||||
]
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
# ECS Framework for Cocos Creator - 开发扩展插件
|
||||
|
||||
专业的ECS框架开发助手,为Cocos Creator提供完整的实体组件系统(ECS)开发工具链。
|
||||
|
||||
## 🎯 主要功能
|
||||
|
||||
### 📦 一键安装管理
|
||||
- **自动检测**:实时检测ECS框架安装状态和版本信息
|
||||
- **一键安装**:快速安装 `@esengine/ecs-framework` 到当前项目
|
||||
- **版本管理**:自动检查更新,支持一键更新到最新版本
|
||||
- **智能卸载**:安全卸载框架,保护项目完整性
|
||||
|
||||
### 🚀 代码生成器
|
||||
- **智能生成**:输入功能名称,自动生成对应的组件和系统代码
|
||||
- **多种系统类型**:支持EntitySystem、ProcessingSystem、IntervalSystem、PassiveSystem
|
||||
- **组件配置**:可选择添加属性、注释等定制化选项
|
||||
- **组件过滤**:支持生成带组件过滤的高级系统
|
||||
|
||||
### 🛠️ 项目模板
|
||||
- **快速启动**:一键生成完整的ECS项目结构
|
||||
- **预设组件**:包含位置、速度、Cocos节点等常用组件
|
||||
- **系统示例**:提供移动系统、节点同步系统等实用示例
|
||||
- **工厂模式**:包含实体工厂和场景管理器模板
|
||||
|
||||
### 🔍 调试工具
|
||||
- **实时监控**:查看ECS框架运行状态和性能数据
|
||||
- **组件池监控**:实时监控组件对象池使用情况
|
||||
- **性能分析**:提供详细的性能统计和优化建议
|
||||
|
||||
## 📋 面板介绍
|
||||
|
||||
### 欢迎面板
|
||||
- ECS框架安装状态检测
|
||||
- 一键安装、更新、卸载操作
|
||||
- 项目模板生成
|
||||
- 快速访问文档和GitHub
|
||||
|
||||
### 代码生成器
|
||||
- 可视化代码生成界面
|
||||
- 实时预览生成的代码结构
|
||||
- 支持批量生成多个文件
|
||||
|
||||
### 调试面板
|
||||
- 实时性能监控
|
||||
- 组件池状态查看
|
||||
- 系统运行统计
|
||||
|
||||
## 🔧 开发环境
|
||||
|
||||
- **Cocos Creator**: >= 3.8.6
|
||||
- **Node.js**: >= 14.0.0
|
||||
- **依赖框架**: @esengine/ecs-framework
|
||||
|
||||
## 📥 安装使用
|
||||
|
||||
1. 将插件复制到项目的 `extensions` 目录
|
||||
2. 在Cocos Creator中启用插件
|
||||
3. 通过菜单 `面板 -> ECS Framework -> 欢迎面板` 打开主界面
|
||||
4. 按照界面提示安装ECS框架并开始开发
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
1. **安装框架**:在欢迎面板点击"安装 ECS Framework"
|
||||
2. **创建模板**:点击"创建ECS模板"生成项目结构
|
||||
3. **生成代码**:使用代码生成器快速创建组件和系统
|
||||
4. **开始开发**:基于生成的模板开始您的ECS游戏开发
|
||||
|
||||
## 📚 更多资源
|
||||
|
||||
- **GitHub仓库**:[https://github.com/esengine/ecs-framework](https://github.com/esengine/ecs-framework)
|
||||
- **完整文档**:包含详细的API文档和教程
|
||||
- **技术交流**:加入QQ群获取技术支持和交流
|
||||
|
||||
## ⭐ 特色优势
|
||||
|
||||
- **零配置**:开箱即用,无需复杂配置
|
||||
- **可视化**:图形化界面,操作简单直观
|
||||
- **高效率**:大幅减少重复代码编写
|
||||
- **专业性**:基于成熟的ECS框架设计模式
|
||||
|
||||
让ECS开发变得简单高效,专注于游戏逻辑而非框架配置!
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"$schema": "https://schemastore.azurewebsites.net/schemas/json/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"experimentalDecorators": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./source",
|
||||
"types": [
|
||||
"node",
|
||||
"@cocos/creator-types/editor",
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
description: "Professional ECS Framework Development Assistant: One-click installation of @esengine/ecs-framework, intelligent code generator for quick creation of components and systems, project template generation, real-time status detection and version management. Provides welcome panel, debug panel, code generator and behavior tree AI component library to make ECS development in Cocos Creator more efficient and convenient.",
|
||||
|
||||
open_panel: "Default Panel",
|
||||
send_to_panel: "Send message to panel",
|
||||
|
||||
menu: {
|
||||
panel: "Panel",
|
||||
develop: "Develop",
|
||||
create: "Create",
|
||||
open: "Open"
|
||||
}
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
// 插件描述
|
||||
description: "专业的ECS框架开发助手:一键安装@esengine/ecs-framework,智能代码生成器快速创建组件和系统,项目模板生成,实时状态检测和版本管理。提供欢迎面板、调试面板、代码生成器和行为树AI组件库,让Cocos Creator的ECS开发更高效便捷。",
|
||||
|
||||
// 面板相关
|
||||
open_panel: "默认面板",
|
||||
send_to_panel: "发送消息给面板",
|
||||
|
||||
// 菜单相关
|
||||
menu: {
|
||||
panel: "面板",
|
||||
develop: "开发",
|
||||
create: "创建",
|
||||
open: "打开"
|
||||
}
|
||||
};
|
||||
@@ -1,318 +0,0 @@
|
||||
{
|
||||
"name": "cocos-ecs-extension",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cocos-ecs-extension",
|
||||
"version": "1.0.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"fs-extra": "^10.0.0",
|
||||
"vue": "^3.1.4",
|
||||
"ws": "^8.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cocos/creator-types": "^3.8.6",
|
||||
"@types/fs-extra": "^9.0.5",
|
||||
"@types/node": "^18.17.1",
|
||||
"@types/ws": "^8.5.10",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.23.0",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cocos/creator-types": {
|
||||
"version": "3.8.6",
|
||||
"resolved": "https://registry.npmjs.org/@cocos/creator-types/-/creator-types-3.8.6.tgz",
|
||||
"integrity": "sha512-hyZ4aoqqLxoRtKbBLSJM5RgtK3oGOlTEryHDcyH4znq3h9cFk+MSbQC2aJHvK5/bMlJzsZ641/hD77RGSrvo8Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/fs-extra": {
|
||||
"version": "9.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
|
||||
"integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.19.111",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.111.tgz",
|
||||
"integrity": "sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.3.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.21.3",
|
||||
"@vue/shared": "3.3.4",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.3.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.3.4",
|
||||
"@vue/shared": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.3.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.20.15",
|
||||
"@vue/compiler-core": "3.3.4",
|
||||
"@vue/compiler-dom": "3.3.4",
|
||||
"@vue/compiler-ssr": "3.3.4",
|
||||
"@vue/reactivity-transform": "3.3.4",
|
||||
"@vue/shared": "3.3.4",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.0",
|
||||
"postcss": "^8.1.10",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.3.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.3.4",
|
||||
"@vue/shared": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.3.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity-transform": {
|
||||
"version": "3.3.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.20.15",
|
||||
"@vue/compiler-core": "3.3.4",
|
||||
"@vue/shared": "3.3.4",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.3.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.3.4",
|
||||
"@vue/shared": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.3.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/runtime-core": "3.3.4",
|
||||
"@vue/shared": "3.3.4",
|
||||
"csstype": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.3.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.3.4",
|
||||
"@vue/shared": "3.3.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.3.4",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "10.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.6",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.30",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.3.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.3.4",
|
||||
"@vue/compiler-sfc": "3.3.4",
|
||||
"@vue/runtime-dom": "3.3.4",
|
||||
"@vue/server-renderer": "3.3.4",
|
||||
"@vue/shared": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.2",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
|
||||
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
{
|
||||
"$schema": "./@types/schema/package/index.json",
|
||||
"package_version": 2,
|
||||
"name": "cocos-ecs-extension",
|
||||
"version": "1.0.0",
|
||||
"author": "esengine",
|
||||
"editor": ">=3.8.6",
|
||||
"scripts": {
|
||||
"preinstall": "node ./scripts/preinstall.js",
|
||||
"build": "npx tsc"
|
||||
},
|
||||
"description": "i18n:cocos-ecs-extension.description",
|
||||
"main": "./dist/main.js",
|
||||
"dependencies": {
|
||||
"vue": "^3.1.4",
|
||||
"fs-extra": "^10.0.0",
|
||||
"ws": "^8.14.2",
|
||||
"adm-zip": "^0.5.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cocos/creator-types": "^3.8.6",
|
||||
"@types/fs-extra": "^9.0.5",
|
||||
"@types/node": "^18.17.1",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@types/adm-zip": "^0.5.0",
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"panels": {
|
||||
"default": {
|
||||
"title": "ECS Framework - 欢迎面板",
|
||||
"type": "dockable",
|
||||
"main": "dist/panels/default/index.js",
|
||||
"size": {
|
||||
"min-width": 450,
|
||||
"min-height": 600,
|
||||
"width": 850,
|
||||
"height": 800
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"title": "ECS Framework - 调试面板",
|
||||
"type": "dockable",
|
||||
"main": "dist/panels/debug/index.js",
|
||||
"size": {
|
||||
"min-width": 400,
|
||||
"min-height": 500,
|
||||
"width": 500,
|
||||
"height": 600
|
||||
}
|
||||
},
|
||||
"generator": {
|
||||
"title": "ECS Framework - 代码生成器",
|
||||
"type": "dockable",
|
||||
"main": "dist/panels/generator/index.js",
|
||||
"size": {
|
||||
"min-width": 600,
|
||||
"min-height": 500,
|
||||
"width": 900,
|
||||
"height": 700
|
||||
}
|
||||
},
|
||||
"behavior-tree": {
|
||||
"title": "ECS Framework - 行为树AI组件库",
|
||||
"type": "dockable",
|
||||
"main": "dist/panels/behavior-tree/index.js",
|
||||
"size": {
|
||||
"min-width": 700,
|
||||
"min-height": 600,
|
||||
"width": 1000,
|
||||
"height": 800
|
||||
}
|
||||
}
|
||||
},
|
||||
"contributions": {
|
||||
"scene": {
|
||||
"script": "./dist/scene.js"
|
||||
},
|
||||
"menu": [
|
||||
{
|
||||
"path": "i18n:menu.panel/ECS Framework",
|
||||
"label": "欢迎面板",
|
||||
"message": "open-panel"
|
||||
},
|
||||
{
|
||||
"path": "i18n:menu.panel/ECS Framework",
|
||||
"label": "调试面板",
|
||||
"message": "open-debug"
|
||||
},
|
||||
{
|
||||
"path": "i18n:menu.panel/ECS Framework",
|
||||
"label": "代码生成器",
|
||||
"message": "open-generator"
|
||||
},
|
||||
{
|
||||
"path": "i18n:menu.panel/ECS Framework",
|
||||
"label": "行为树AI组件库",
|
||||
"message": "open-behavior-tree"
|
||||
},
|
||||
{
|
||||
"path": "i18n:menu.develop/ECS Framework",
|
||||
"label": "ECS 开发工具",
|
||||
"message": "open-panel"
|
||||
},
|
||||
{
|
||||
"path": "i18n:menu.panel/ECS Framework",
|
||||
"label": "检查更新",
|
||||
"message": "check-plugin-updates"
|
||||
}
|
||||
],
|
||||
"assets": {
|
||||
"menu": {
|
||||
"methods": "./dist/assets-menu.js",
|
||||
"assetMenu": "onAssetMenu"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"open-panel": {
|
||||
"methods": [
|
||||
"openPanel"
|
||||
]
|
||||
},
|
||||
"install-ecs-framework": {
|
||||
"methods": [
|
||||
"install-ecs-framework"
|
||||
]
|
||||
},
|
||||
"update-ecs-framework": {
|
||||
"methods": [
|
||||
"update-ecs-framework"
|
||||
]
|
||||
},
|
||||
"uninstall-ecs-framework": {
|
||||
"methods": [
|
||||
"uninstall-ecs-framework"
|
||||
]
|
||||
},
|
||||
"open-documentation": {
|
||||
"methods": [
|
||||
"open-documentation"
|
||||
]
|
||||
},
|
||||
"create-ecs-template": {
|
||||
"methods": [
|
||||
"create-ecs-template"
|
||||
]
|
||||
},
|
||||
"open-github": {
|
||||
"methods": [
|
||||
"open-github"
|
||||
]
|
||||
},
|
||||
"open-qq-group": {
|
||||
"methods": [
|
||||
"open-qq-group"
|
||||
]
|
||||
},
|
||||
"open-debug": {
|
||||
"methods": [
|
||||
"open-debug"
|
||||
]
|
||||
},
|
||||
"open-generator": {
|
||||
"methods": [
|
||||
"open-generator"
|
||||
]
|
||||
},
|
||||
"open-behavior-tree": {
|
||||
"methods": [
|
||||
"open-behavior-tree"
|
||||
]
|
||||
},
|
||||
"install-behavior-tree": {
|
||||
"methods": [
|
||||
"install-behavior-tree"
|
||||
]
|
||||
},
|
||||
"update-behavior-tree": {
|
||||
"methods": [
|
||||
"update-behavior-tree"
|
||||
]
|
||||
},
|
||||
"check-behavior-tree-installed": {
|
||||
"methods": [
|
||||
"check-behavior-tree-installed"
|
||||
]
|
||||
},
|
||||
"check-plugin-updates": {
|
||||
"methods": [
|
||||
"check-plugin-updates"
|
||||
]
|
||||
},
|
||||
"set-hot-update-config": {
|
||||
"methods": [
|
||||
"set-hot-update-config"
|
||||
]
|
||||
},
|
||||
"get-hot-update-config": {
|
||||
"methods": [
|
||||
"get-hot-update-config"
|
||||
]
|
||||
},
|
||||
"open-behavior-tree-docs": {
|
||||
"methods": [
|
||||
"open-behavior-tree-docs"
|
||||
]
|
||||
},
|
||||
"create-behavior-tree-file": {
|
||||
"methods": [
|
||||
"create-behavior-tree-file"
|
||||
]
|
||||
},
|
||||
"load-behavior-tree-file": {
|
||||
"methods": [
|
||||
"load-behavior-tree-file"
|
||||
]
|
||||
},
|
||||
"create-behavior-tree-from-editor": {
|
||||
"methods": [
|
||||
"create-behavior-tree-from-editor"
|
||||
]
|
||||
},
|
||||
"overwrite-behavior-tree-file": {
|
||||
"methods": [
|
||||
"overwrite-behavior-tree-file"
|
||||
]
|
||||
},
|
||||
"behavior-tree-panel-load-file": {
|
||||
"methods": [
|
||||
"behavior-tree.loadBehaviorTreeFile"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
const readFileSync=require("fs")["readFileSync"],join=require("path")["join"],spawnSync=require("child_process")["spawnSync"],PATH={packageJSON:join(__dirname,"../package.json")};function checkCreatorTypesVersion(e){var o="win32"===process.platform?"npm.cmd":"npm";let n=spawnSync(o,["view","@cocos/creator-types","versions"]).stdout.toString();try{n=JSON.parse(listString)}catch(e){}return!!n.includes(e)}try{const e=readFileSync(PATH.packageJSON,"utf8"),f=JSON.parse(e),g=f.devDependencies["@cocos/creator-types"].replace(/^[^\d]+/,"");checkCreatorTypesVersion(g)||(console.log("[33mWarning:[0m"),console.log(" @en"),console.log(" Version check of @cocos/creator-types failed."),console.log(` The definition of ${g} has not been released yet. Please export the definition to the ./node_modules directory by selecting "Developer -> Export Interface Definition" in the menu of the Creator editor.`),console.log(" The definition of the corresponding version will be released on npm after the editor is officially released."),console.log(" @zh"),console.log(" @cocos/creator-types 版本检查失败。"),console.log(` ${g} 定义还未发布,请先通过 Creator 编辑器菜单 "开发者 -> 导出接口定义",导出定义到 ./node_modules 目录。`),console.log(" 对应版本的定义会在编辑器正式发布后同步发布到 npm 上。"))}catch(e){console.error(e)}
|
||||
@@ -1,274 +0,0 @@
|
||||
import { ensureDir, writeFile } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
|
||||
/**
|
||||
* 代码生成器工具类
|
||||
* 用于生成基础的ECS框架代码
|
||||
*/
|
||||
|
||||
interface ComponentOptions {
|
||||
includeComments: boolean;
|
||||
addProperties: string[];
|
||||
}
|
||||
|
||||
interface SystemOptions {
|
||||
includeComments: boolean;
|
||||
systemType: 'EntitySystem' | 'ProcessingSystem' | 'IntervalSystem' | 'PassiveSystem';
|
||||
requiredComponents: string[];
|
||||
}
|
||||
|
||||
export class CodeGenerator {
|
||||
|
||||
/**
|
||||
* 生成组件代码
|
||||
*/
|
||||
public async generateComponent(
|
||||
name: string,
|
||||
targetDir: string,
|
||||
options: ComponentOptions = {
|
||||
includeComments: true,
|
||||
addProperties: []
|
||||
}
|
||||
): Promise<void> {
|
||||
const className = `${name}Component`;
|
||||
const fileName = `${className}.ts`;
|
||||
const filePath = join(targetDir, fileName);
|
||||
|
||||
await ensureDir(targetDir);
|
||||
|
||||
const comments = options.includeComments ? this.generateComponentComments(className) : '';
|
||||
const properties = this.generateComponentProperties(options.addProperties);
|
||||
|
||||
const content = `import { Component } from '@esengine/ecs-framework';
|
||||
|
||||
${comments}
|
||||
export class ${className} extends Component {
|
||||
${properties}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置组件状态
|
||||
*/
|
||||
public reset(): void {
|
||||
// 重置组件属性到默认值
|
||||
${this.generateResetCode(options.addProperties)}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
await writeFile(filePath, content, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成系统代码
|
||||
*/
|
||||
public async generateSystem(
|
||||
name: string,
|
||||
targetDir: string,
|
||||
options: SystemOptions = {
|
||||
includeComments: true,
|
||||
systemType: 'EntitySystem',
|
||||
requiredComponents: []
|
||||
}
|
||||
): Promise<void> {
|
||||
const className = `${name}System`;
|
||||
const fileName = `${className}.ts`;
|
||||
const filePath = join(targetDir, fileName);
|
||||
|
||||
await ensureDir(targetDir);
|
||||
|
||||
const comments = options.includeComments ? this.generateSystemComments(className, options.systemType) : '';
|
||||
const imports = this.getSystemImports(options.systemType, options.requiredComponents);
|
||||
const matcherSetup = options.requiredComponents.length > 0 ?
|
||||
`Matcher.empty().all(${options.requiredComponents.join(', ')})` :
|
||||
`Matcher.empty()`;
|
||||
|
||||
const processMethod = this.generateProcessMethod(options.systemType, options.requiredComponents, className);
|
||||
|
||||
const content = `${imports}
|
||||
|
||||
${comments}
|
||||
export class ${className} extends ${options.systemType} {
|
||||
|
||||
constructor() {
|
||||
super(${matcherSetup}${options.systemType === 'IntervalSystem' ? ', 1000 / 60' : ''})${options.systemType === 'IntervalSystem' ? '; // 60fps' : ';'}
|
||||
}
|
||||
|
||||
${processMethod}
|
||||
|
||||
/**
|
||||
* 系统开始时调用
|
||||
*/
|
||||
public begin(): void {
|
||||
super.begin();
|
||||
// 添加系统初始化逻辑
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统结束时调用
|
||||
*/
|
||||
public end(): void {
|
||||
// 添加系统清理逻辑
|
||||
super.end();
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
await writeFile(filePath, content, 'utf-8');
|
||||
}
|
||||
|
||||
// ============ 辅助方法 ============
|
||||
|
||||
private generateComponentComments(className: string): string {
|
||||
return `/**
|
||||
* ${className}
|
||||
*
|
||||
* 组件描述
|
||||
*
|
||||
* @example
|
||||
* \`\`\`typescript
|
||||
* const entity = scene.createEntity("Example");
|
||||
* const component = entity.addComponent(new ${className}());
|
||||
* \`\`\`
|
||||
*/`;
|
||||
}
|
||||
|
||||
private generateSystemComments(className: string, systemType: string): string {
|
||||
const descriptions = {
|
||||
'EntitySystem': '处理拥有特定组件的实体',
|
||||
'ProcessingSystem': '执行全局游戏逻辑',
|
||||
'IntervalSystem': '按时间间隔处理实体',
|
||||
'PassiveSystem': '被动响应事件或手动调用'
|
||||
};
|
||||
|
||||
return `/**
|
||||
* ${className}
|
||||
*
|
||||
* ${descriptions[systemType as keyof typeof descriptions] || '处理游戏逻辑'}
|
||||
*
|
||||
* @example
|
||||
* \`\`\`typescript
|
||||
* const system = new ${className}();
|
||||
* scene.addEntityProcessor(system);
|
||||
* \`\`\`
|
||||
*/`;
|
||||
}
|
||||
|
||||
private generateComponentProperties(properties: string[]): string {
|
||||
if (properties.length === 0) {
|
||||
return ' // 添加组件属性\n // public value: number = 0;';
|
||||
}
|
||||
|
||||
return properties.map(prop => {
|
||||
const [name, type = 'number', defaultValue = '0'] = prop.split(':');
|
||||
return ` public ${name}: ${type} = ${defaultValue};`;
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
private generateResetCode(properties: string[]): string {
|
||||
if (properties.length === 0) {
|
||||
return ' // this.value = 0;';
|
||||
}
|
||||
|
||||
return properties.map(prop => {
|
||||
const [name, , defaultValue = '0'] = prop.split(':');
|
||||
return ` this.${name} = ${defaultValue};`;
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
private getSystemImports(systemType: string, requiredComponents: string[]): string {
|
||||
const imports = [systemType, 'Entity'];
|
||||
|
||||
// 所有系统类型都可能需要Matcher来过滤组件
|
||||
if (requiredComponents.length > 0 || systemType === 'EntitySystem' || systemType === 'IntervalSystem' || systemType === 'PassiveSystem') {
|
||||
imports.push('Matcher');
|
||||
}
|
||||
|
||||
return `import { ${imports.join(', ')} } from '@esengine/ecs-framework';${requiredComponents.length > 0 ? '\n' + this.generateComponentImports(requiredComponents) : ''}`;
|
||||
}
|
||||
|
||||
private generateComponentImports(components: string[]): string {
|
||||
return components.map(comp => `import { ${comp} } from '../components/${comp}';`).join('\n');
|
||||
}
|
||||
|
||||
private generateProcessMethod(systemType: string, requiredComponents: string[], className: string): string {
|
||||
switch (systemType) {
|
||||
case 'EntitySystem':
|
||||
return ` protected process(entities: Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
this.processEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private processEntity(entity: Entity): void {
|
||||
${this.generateProcessingLogic(requiredComponents)}
|
||||
}`;
|
||||
|
||||
case 'ProcessingSystem':
|
||||
return ` public processSystem(): void {
|
||||
// 添加全局系统逻辑
|
||||
console.log('${className} processSystem called');
|
||||
}`;
|
||||
|
||||
case 'IntervalSystem':
|
||||
return ` protected process(entities: Entity[]): void {
|
||||
const intervalDelta = this.getIntervalDelta();
|
||||
console.log(\`${className} executing with interval delta: \${intervalDelta}\`);
|
||||
|
||||
for (const entity of entities) {
|
||||
this.processEntity(entity, intervalDelta);
|
||||
}
|
||||
}
|
||||
|
||||
private processEntity(entity: Entity, delta: number): void {
|
||||
${this.generateProcessingLogic(requiredComponents)}
|
||||
}`;
|
||||
|
||||
case 'PassiveSystem':
|
||||
return ` /**
|
||||
* 被动系统不主动处理实体
|
||||
* 通常用于响应事件或被其他系统调用
|
||||
*/
|
||||
public processEntity(entity: Entity): void {
|
||||
${this.generateProcessingLogic(requiredComponents)}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发处理
|
||||
*/
|
||||
public trigger(): void {
|
||||
for (const entity of this.entities) {
|
||||
this.processEntity(entity);
|
||||
}
|
||||
}`;
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private generateProcessingLogic(requiredComponents: string[]): string {
|
||||
if (requiredComponents.length === 0) {
|
||||
return ' // 添加处理逻辑';
|
||||
}
|
||||
|
||||
const componentVars = requiredComponents.map((comp: string) => {
|
||||
const varName = comp.replace('Component', '').toLowerCase();
|
||||
return ` const ${varName} = entity.getComponent(${comp});`;
|
||||
}).join('\n');
|
||||
|
||||
const nullCheck = requiredComponents.map((comp: string) => {
|
||||
const varName = comp.replace('Component', '').toLowerCase();
|
||||
return varName;
|
||||
}).join(' && ');
|
||||
|
||||
return `${componentVars}
|
||||
|
||||
if (${nullCheck}) {
|
||||
// 添加处理逻辑
|
||||
}`;
|
||||
}
|
||||
}
|
||||
@@ -1,436 +0,0 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
/**
|
||||
* ECS启动模板生成器
|
||||
* 生成最基础的ECS框架启动模板,不包含业务逻辑
|
||||
*/
|
||||
export class TemplateGenerator {
|
||||
private projectPath: string;
|
||||
private ecsDir: string;
|
||||
|
||||
constructor(projectPath: string) {
|
||||
this.projectPath = projectPath;
|
||||
this.ecsDir = path.join(projectPath, 'assets', 'scripts', 'ecs');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已经存在ECS模板
|
||||
*/
|
||||
public checkTemplateExists(): boolean {
|
||||
return fs.existsSync(this.ecsDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已存在的文件列表
|
||||
*/
|
||||
public getExistingFiles(): string[] {
|
||||
if (!this.checkTemplateExists()) return [];
|
||||
|
||||
const files: string[] = [];
|
||||
this.scanDirectory(this.ecsDir, '', files);
|
||||
return files;
|
||||
}
|
||||
|
||||
private scanDirectory(dirPath: string, relativePath: string, files: string[]): void {
|
||||
if (!fs.existsSync(dirPath)) return;
|
||||
|
||||
const items = fs.readdirSync(dirPath);
|
||||
for (const item of items) {
|
||||
const fullPath = path.join(dirPath, item);
|
||||
const relativeFilePath = relativePath ? `${relativePath}/${item}` : item;
|
||||
|
||||
if (fs.statSync(fullPath).isDirectory()) {
|
||||
this.scanDirectory(fullPath, relativeFilePath, files);
|
||||
} else {
|
||||
files.push(relativeFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除现有的ECS模板
|
||||
*/
|
||||
public removeExistingTemplate(): void {
|
||||
if (fs.existsSync(this.ecsDir)) {
|
||||
fs.rmSync(this.ecsDir, { recursive: true, force: true });
|
||||
console.log('Removed existing ECS template');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建ECS启动模板
|
||||
*/
|
||||
public createTemplate(): void {
|
||||
// 创建目录结构
|
||||
this.createDirectories();
|
||||
|
||||
// 创建ECS启动管理器
|
||||
this.createECSManager();
|
||||
|
||||
// 创建基础游戏场景
|
||||
this.createBaseGameScene();
|
||||
|
||||
// 创建README文档
|
||||
this.createReadme();
|
||||
|
||||
console.log('ECS启动模板创建成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建目录结构
|
||||
*/
|
||||
private createDirectories(): void {
|
||||
const dirs = [
|
||||
this.ecsDir,
|
||||
path.join(this.ecsDir, 'scenes'),
|
||||
path.join(this.ecsDir, 'components'),
|
||||
path.join(this.ecsDir, 'systems')
|
||||
];
|
||||
|
||||
dirs.forEach(dir => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
console.log(`Created directory: ${path.relative(this.projectPath, dir)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建ECS管理器
|
||||
*/
|
||||
private createECSManager(): void {
|
||||
this.writeFile(path.join(this.ecsDir, 'ECSManager.ts'), `import { Core } from '@esengine/ecs-framework';
|
||||
import { Component, _decorator } from 'cc';
|
||||
import { GameScene } from './scenes/GameScene';
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* ECS管理器 - Cocos Creator组件
|
||||
* 将此组件添加到场景中的任意节点上即可启动ECS框架
|
||||
*
|
||||
* 使用说明:
|
||||
* 1. 在Cocos Creator场景中创建一个空节点
|
||||
* 2. 将此ECSManager组件添加到该节点
|
||||
* 3. 运行场景即可自动启动ECS框架
|
||||
*/
|
||||
@ccclass('ECSManager')
|
||||
export class ECSManager extends Component {
|
||||
|
||||
@property({
|
||||
tooltip: '是否启用调试模式(建议开发阶段开启)'
|
||||
})
|
||||
public debugMode: boolean = true;
|
||||
|
||||
private isInitialized: boolean = false;
|
||||
|
||||
/**
|
||||
* 组件启动时初始化ECS
|
||||
*/
|
||||
start() {
|
||||
this.initializeECS();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化ECS框架
|
||||
*/
|
||||
private initializeECS(): void {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
console.log('🎮 正在初始化ECS框架...');
|
||||
|
||||
try {
|
||||
// 1. 创建Core实例,启用调试功能
|
||||
if (this.debugMode) {
|
||||
Core.create({
|
||||
debugConfig: {
|
||||
enabled: true,
|
||||
websocketUrl: 'ws://localhost:8080/ecs-debug',
|
||||
autoReconnect: true,
|
||||
updateInterval: 100,
|
||||
channels: {
|
||||
entities: true,
|
||||
systems: true,
|
||||
performance: true,
|
||||
components: true,
|
||||
scenes: true
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log('🔧 ECS调试模式已启用,可在Cocos Creator扩展面板中查看调试信息');
|
||||
} else {
|
||||
Core.create(false);
|
||||
}
|
||||
|
||||
// 2. 创建游戏场景
|
||||
const gameScene = new GameScene();
|
||||
|
||||
// 3. 设置为当前场景(会自动调用scene.begin())
|
||||
Core.scene = gameScene;
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log('✅ ECS框架初始化成功!');
|
||||
console.log('📖 请查看 assets/scripts/ecs/README.md 了解如何添加组件和系统');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ ECS框架初始化失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每帧更新ECS框架
|
||||
*/
|
||||
update(deltaTime: number) {
|
||||
if (this.isInitialized) {
|
||||
// 更新ECS核心系统
|
||||
Core.update(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件销毁时清理ECS
|
||||
*/
|
||||
onDestroy() {
|
||||
if (this.isInitialized) {
|
||||
console.log('🧹 清理ECS框架...');
|
||||
// ECS框架会自动处理场景清理
|
||||
this.isInitialized = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建基础游戏场景
|
||||
*/
|
||||
private createBaseGameScene(): void {
|
||||
this.writeFile(path.join(this.ecsDir, 'scenes', 'GameScene.ts'), `import { Scene } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 游戏场景
|
||||
*
|
||||
* 这是您的主游戏场景。在这里可以:
|
||||
* - 添加游戏系统
|
||||
* - 创建初始实体
|
||||
* - 设置场景参数
|
||||
*/
|
||||
export class GameScene extends Scene {
|
||||
|
||||
/**
|
||||
* 场景初始化
|
||||
* 在场景创建时调用,用于设置基础配置
|
||||
*/
|
||||
public initialize(): void {
|
||||
super.initialize();
|
||||
|
||||
// 设置场景名称
|
||||
this.name = "MainGameScene";
|
||||
|
||||
console.log('🎯 游戏场景已创建');
|
||||
|
||||
// TODO: 在这里添加您的游戏系统
|
||||
// 例如:this.addEntityProcessor(new MovementSystem());
|
||||
|
||||
// TODO: 在这里创建初始实体
|
||||
// 例如:this.createEntity("Player");
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景开始运行
|
||||
* 在场景开始时调用,用于执行启动逻辑
|
||||
*/
|
||||
public onStart(): void {
|
||||
super.onStart();
|
||||
|
||||
console.log('🚀 游戏场景已启动');
|
||||
|
||||
// TODO: 在这里添加场景启动逻辑
|
||||
// 例如:创建UI、播放音乐、初始化游戏状态等
|
||||
}
|
||||
|
||||
/**
|
||||
* 场景卸载
|
||||
* 在场景结束时调用,用于清理资源
|
||||
*/
|
||||
public unload(): void {
|
||||
console.log('🛑 游戏场景已结束');
|
||||
|
||||
// TODO: 在这里添加清理逻辑
|
||||
// 例如:清理缓存、释放资源等
|
||||
|
||||
super.unload();
|
||||
}
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建README文档
|
||||
*/
|
||||
private createReadme(): void {
|
||||
this.writeFile(path.join(this.ecsDir, 'README.md'), `# ECS框架启动模板
|
||||
|
||||
欢迎使用ECS框架!这是一个最基础的启动模板,帮助您快速开始ECS项目开发。
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
\`\`\`
|
||||
ecs/
|
||||
├── components/ # 组件目录(请在此添加您的组件)
|
||||
├── systems/ # 系统目录(请在此添加您的系统)
|
||||
├── scenes/ # 场景目录
|
||||
│ └── GameScene.ts # 主游戏场景
|
||||
├── ECSManager.ts # ECS管理器组件
|
||||
└── README.md # 本文档
|
||||
\`\`\`
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 启动ECS框架
|
||||
|
||||
ECS框架已经配置完成!您只需要:
|
||||
|
||||
1. 在Cocos Creator中打开您的场景
|
||||
2. 创建一个空节点(例如命名为"ECSManager")
|
||||
3. 将 \`ECSManager\` 组件添加到该节点
|
||||
4. 运行场景,ECS框架将自动启动
|
||||
|
||||
### 2. 查看控制台输出
|
||||
|
||||
如果一切正常,您将在控制台看到:
|
||||
|
||||
\`\`\`
|
||||
🎮 正在初始化ECS框架...
|
||||
🔧 ECS调试模式已启用,可在Cocos Creator扩展面板中查看调试信息
|
||||
🎯 游戏场景已创建
|
||||
✅ ECS框架初始化成功!
|
||||
🚀 游戏场景已启动
|
||||
\`\`\`
|
||||
|
||||
### 3. 使用调试面板
|
||||
|
||||
ECS框架已启用调试功能,您可以:
|
||||
|
||||
1. 在Cocos Creator编辑器菜单中选择 "扩展" → "ECS Framework" → "调试面板"
|
||||
2. 调试面板将显示实时的ECS运行状态:
|
||||
- 实体数量和状态
|
||||
- 系统执行信息
|
||||
- 性能监控数据
|
||||
- 组件统计信息
|
||||
|
||||
**注意**:调试功能会消耗一定性能,正式发布时建议关闭调试模式。
|
||||
|
||||
## 📚 下一步开发
|
||||
|
||||
### 创建您的第一个组件
|
||||
|
||||
在 \`components/\` 目录下创建组件:
|
||||
|
||||
\`\`\`typescript
|
||||
// components/PositionComponent.ts
|
||||
import { Component } from '@esengine/ecs-framework';
|
||||
import { Vec3 } from 'cc';
|
||||
|
||||
export class PositionComponent extends Component {
|
||||
public position: Vec3 = new Vec3();
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.position.set(x, y, z);
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### 创建您的第一个系统
|
||||
|
||||
在 \`systems/\` 目录下创建系统:
|
||||
|
||||
\`\`\`typescript
|
||||
// systems/MovementSystem.ts
|
||||
import { EntitySystem, Entity, Matcher } from '@esengine/ecs-framework';
|
||||
import { PositionComponent } from '../components/PositionComponent';
|
||||
|
||||
export class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(PositionComponent));
|
||||
}
|
||||
|
||||
protected process(entities: Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(PositionComponent);
|
||||
if (position) {
|
||||
// TODO: 在这里编写移动逻辑
|
||||
console.log(\`实体 \${entity.name} 位置: \${position.position}\`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### 在场景中注册系统
|
||||
|
||||
在 \`scenes/GameScene.ts\` 的 \`initialize()\` 方法中添加:
|
||||
|
||||
\`\`\`typescript
|
||||
import { MovementSystem } from '../systems/MovementSystem';
|
||||
|
||||
public initialize(): void {
|
||||
super.initialize();
|
||||
this.name = "MainGameScene";
|
||||
|
||||
// 添加系统
|
||||
this.addEntityProcessor(new MovementSystem());
|
||||
|
||||
// 创建测试实体
|
||||
const testEntity = this.createEntity("TestEntity");
|
||||
testEntity.addComponent(new PositionComponent(0, 0, 0));
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## 🔗 学习资源
|
||||
|
||||
- [ECS框架完整文档](https://github.com/esengine/ecs-framework)
|
||||
- [ECS概念详解](https://github.com/esengine/ecs-framework/blob/master/docs/concepts-explained.md)
|
||||
- [新手教程](https://github.com/esengine/ecs-framework/blob/master/docs/beginner-tutorials.md)
|
||||
- [组件设计指南](https://github.com/esengine/ecs-framework/blob/master/docs/component-design-guide.md)
|
||||
- [系统开发指南](https://github.com/esengine/ecs-framework/blob/master/docs/system-guide.md)
|
||||
|
||||
## 💡 开发提示
|
||||
|
||||
1. **组件只存储数据**:避免在组件中编写复杂逻辑
|
||||
2. **系统处理逻辑**:所有业务逻辑应该在系统中实现
|
||||
3. **使用Matcher过滤实体**:系统通过Matcher指定需要处理的实体类型
|
||||
4. **性能优化**:大量实体时考虑使用位掩码查询和组件索引
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### Q: 如何创建实体?
|
||||
A: 在场景中使用 \`this.createEntity("实体名称")\`
|
||||
|
||||
### Q: 如何给实体添加组件?
|
||||
A: 使用 \`entity.addComponent(new YourComponent())\`
|
||||
|
||||
### Q: 如何获取实体的组件?
|
||||
A: 使用 \`entity.getComponent(YourComponent)\`
|
||||
|
||||
### Q: 如何删除实体?
|
||||
A: 使用 \`entity.destroy()\` 或 \`this.destroyEntity(entity)\`
|
||||
|
||||
---
|
||||
|
||||
🎮 **开始您的ECS开发之旅吧!**
|
||||
|
||||
如有问题,请查阅官方文档或提交Issue。
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入文件
|
||||
*/
|
||||
private writeFile(filePath: string, content: string): void {
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
console.log(`Created file: ${path.relative(this.projectPath, filePath)}`);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
export function onAssetMenu(assetInfo: any) {
|
||||
console.log('[AssetMenu] onAssetMenu 被调用,资源信息:', assetInfo);
|
||||
console.log('[AssetMenu] assetInfo 完整结构:', JSON.stringify(assetInfo, null, 2));
|
||||
|
||||
const menuItems = [];
|
||||
|
||||
// 检查是否为行为树文件
|
||||
const isTargetFile = (assetInfo && assetInfo.name && assetInfo.name.endsWith('.bt.json')) ||
|
||||
(assetInfo && assetInfo.file && assetInfo.file.endsWith('.bt.json'));
|
||||
|
||||
if (isTargetFile) {
|
||||
console.log('[AssetMenu] 发现 .bt.json 文件,添加菜单项');
|
||||
menuItems.push({
|
||||
label: '用行为树编辑器打开',
|
||||
click() {
|
||||
console.log('[AssetMenu] 菜单项被点击,文件信息:', assetInfo);
|
||||
|
||||
// 直接调用主进程的方法,不需要复杂的序列化
|
||||
try {
|
||||
Editor.Message.send('cocos-ecs-extension', 'load-behavior-tree-file', assetInfo);
|
||||
console.log('[AssetMenu] 消息发送成功');
|
||||
} catch (error) {
|
||||
console.error('[AssetMenu] 消息发送失败:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 在目录中添加创建选项
|
||||
if (assetInfo && assetInfo.isDirectory) {
|
||||
menuItems.push({
|
||||
label: '创建行为树文件',
|
||||
click() {
|
||||
console.log('[AssetMenu] 在目录中创建行为树文件:', assetInfo);
|
||||
try {
|
||||
Editor.Message.send('cocos-ecs-extension', 'create-behavior-tree-file');
|
||||
} catch (error) {
|
||||
console.error('[AssetMenu] 创建消息发送失败:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('[AssetMenu] 返回菜单项数量:', menuItems.length);
|
||||
return menuItems;
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
import { exec } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as fsExtra from 'fs-extra';
|
||||
|
||||
/**
|
||||
* 行为树相关的处理器
|
||||
*/
|
||||
export class BehaviorTreeHandler {
|
||||
/**
|
||||
* 安装行为树AI系统
|
||||
*/
|
||||
static async install(): Promise<boolean> {
|
||||
const projectPath = Editor.Project.path;
|
||||
const command = 'npm install @esengine/ai';
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error('AI系统安装失败:', error.message);
|
||||
resolve(false);
|
||||
} else {
|
||||
// 验证安装是否成功
|
||||
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ai');
|
||||
const installSuccess = fs.existsSync(nodeModulesPath);
|
||||
|
||||
if (!installSuccess) {
|
||||
console.warn('安装完成但未找到AI系统目录,请检查网络连接');
|
||||
}
|
||||
|
||||
resolve(installSuccess);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新行为树AI系统
|
||||
*/
|
||||
static async update(): Promise<boolean> {
|
||||
const projectPath = Editor.Project.path;
|
||||
const command = 'npm update @esengine/ai';
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error('AI系统更新失败:', error.message);
|
||||
resolve(false);
|
||||
} else {
|
||||
// 验证更新是否成功
|
||||
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ai');
|
||||
const updateSuccess = fs.existsSync(nodeModulesPath);
|
||||
|
||||
if (!updateSuccess) {
|
||||
console.warn('更新完成但未找到AI系统目录');
|
||||
}
|
||||
|
||||
resolve(updateSuccess);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查行为树AI是否已安装
|
||||
*/
|
||||
static checkInstalled(): boolean {
|
||||
try {
|
||||
const projectPath = Editor.Project.path;
|
||||
const packageJsonPath = path.join(projectPath, 'package.json');
|
||||
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
||||
|
||||
return '@esengine/ai' in dependencies;
|
||||
} catch (error) {
|
||||
console.error('检查AI系统安装状态失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开行为树文档
|
||||
*/
|
||||
static openDocumentation(): void {
|
||||
const url = 'https://github.com/esengine/ai/blob/master/README.md';
|
||||
|
||||
try {
|
||||
const { shell } = require('electron');
|
||||
shell.openExternal(url);
|
||||
console.log('Behavior Tree documentation opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open Behavior Tree documentation:', error);
|
||||
Editor.Dialog.info('打开行为树文档', {
|
||||
detail: `请手动访问以下链接查看文档:\n\n${url}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建行为树文件
|
||||
*/
|
||||
static async createFile(assetInfo?: any): Promise<void> {
|
||||
try {
|
||||
const projectPath = Editor.Project.path;
|
||||
const assetsPath = path.join(projectPath, 'assets');
|
||||
|
||||
// 生成唯一文件名
|
||||
let fileName = 'NewBehaviorTree';
|
||||
let counter = 1;
|
||||
let filePath = path.join(assetsPath, `${fileName}.bt.json`);
|
||||
|
||||
while (fs.existsSync(filePath)) {
|
||||
fileName = `NewBehaviorTree_${counter}`;
|
||||
filePath = path.join(assetsPath, `${fileName}.bt.json`);
|
||||
counter++;
|
||||
}
|
||||
|
||||
// 创建默认的行为树配置
|
||||
const defaultConfig = {
|
||||
version: "1.0.0",
|
||||
type: "behavior-tree",
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
nodeCount: 1
|
||||
},
|
||||
tree: {
|
||||
id: "root",
|
||||
type: "sequence",
|
||||
namespace: "behaviourTree/composites",
|
||||
properties: {},
|
||||
children: []
|
||||
}
|
||||
};
|
||||
|
||||
// 写入文件
|
||||
await fsExtra.writeFile(filePath, JSON.stringify(defaultConfig, null, 2));
|
||||
|
||||
// 刷新资源管理器 - 使用正确的资源路径
|
||||
const relativeAssetPath = path.relative(projectPath, filePath).replace(/\\/g, '/');
|
||||
const dbAssetPath = 'db://' + relativeAssetPath;
|
||||
await Editor.Message.request('asset-db', 'refresh-asset', dbAssetPath);
|
||||
|
||||
console.log(`Behavior tree file created: ${filePath}`);
|
||||
|
||||
Editor.Dialog.info('创建成功', {
|
||||
detail: `行为树文件 "${fileName}.bt.json" 已创建完成!\n\n文件位置:assets/${fileName}.bt.json\n\n您可以右键点击文件选择"用行为树编辑器打开"来编辑它。`,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to create behavior tree file:', error);
|
||||
Editor.Dialog.error('创建失败', {
|
||||
detail: `创建行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开行为树文件
|
||||
*/
|
||||
static async openFile(assetInfo: any): Promise<void> {
|
||||
try {
|
||||
if (!assetInfo || !assetInfo.file) {
|
||||
throw new Error('无效的文件信息');
|
||||
}
|
||||
|
||||
const filePath = assetInfo.file;
|
||||
const fileData = await this.loadFileData(filePath);
|
||||
await this.openPanel();
|
||||
await this.sendDataToPanel(fileData);
|
||||
|
||||
} catch (error) {
|
||||
Editor.Dialog.error('打开失败', {
|
||||
detail: `打开行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取并解析文件数据
|
||||
*/
|
||||
private static async loadFileData(filePath: string): Promise<any> {
|
||||
try {
|
||||
let assetPath = filePath;
|
||||
|
||||
if (path.isAbsolute(filePath)) {
|
||||
const projectPath = Editor.Project.path;
|
||||
if (filePath.startsWith(projectPath)) {
|
||||
assetPath = path.relative(projectPath, filePath);
|
||||
assetPath = assetPath.replace(/\\/g, '/');
|
||||
}
|
||||
}
|
||||
|
||||
if (!assetPath.startsWith('db://')) {
|
||||
assetPath = 'db://' + assetPath;
|
||||
}
|
||||
|
||||
try {
|
||||
const assetInfo = await Editor.Message.request('asset-db', 'query-asset-info', assetPath);
|
||||
|
||||
if (assetInfo && assetInfo.source) {
|
||||
const content = await fsExtra.readFile(assetInfo.source, 'utf8');
|
||||
let fileContent: any;
|
||||
|
||||
try {
|
||||
fileContent = JSON.parse(content);
|
||||
} catch (parseError) {
|
||||
fileContent = {
|
||||
version: "1.0.0",
|
||||
type: "behavior-tree",
|
||||
rawContent: content
|
||||
};
|
||||
}
|
||||
|
||||
const fileData = {
|
||||
...fileContent,
|
||||
_fileInfo: {
|
||||
fileName: path.basename(assetInfo.source, path.extname(assetInfo.source)),
|
||||
filePath: assetInfo.source,
|
||||
assetPath: assetPath
|
||||
}
|
||||
};
|
||||
|
||||
return fileData;
|
||||
}
|
||||
} catch (assetError) {
|
||||
// 资源系统读取失败,尝试直接文件读取
|
||||
}
|
||||
|
||||
const actualFilePath = path.isAbsolute(filePath) ? filePath : path.join(Editor.Project.path, filePath);
|
||||
|
||||
if (!fs.existsSync(actualFilePath)) {
|
||||
throw new Error(`文件不存在: ${actualFilePath}`);
|
||||
}
|
||||
|
||||
const content = await fsExtra.readFile(actualFilePath, 'utf8');
|
||||
let fileContent: any;
|
||||
|
||||
try {
|
||||
fileContent = JSON.parse(content);
|
||||
} catch (parseError) {
|
||||
fileContent = {
|
||||
version: "1.0.0",
|
||||
type: "behavior-tree",
|
||||
rawContent: content
|
||||
};
|
||||
}
|
||||
|
||||
const fileData = {
|
||||
...fileContent,
|
||||
_fileInfo: {
|
||||
fileName: path.basename(actualFilePath, path.extname(actualFilePath)),
|
||||
filePath: actualFilePath
|
||||
}
|
||||
};
|
||||
|
||||
return fileData;
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`文件读取失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开行为树面板
|
||||
*/
|
||||
private static async openPanel(): Promise<void> {
|
||||
await Editor.Panel.open('cocos-ecs-extension.behavior-tree');
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送数据到面板
|
||||
*/
|
||||
private static async sendDataToPanel(fileData: any): Promise<void> {
|
||||
try {
|
||||
const result = await Editor.Message.request('cocos-ecs-extension.behavior-tree', 'loadBehaviorTreeFile', fileData);
|
||||
} catch (error) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
Editor.Message.send('cocos-ecs-extension.behavior-tree', 'loadBehaviorTreeFile', fileData);
|
||||
} catch (delayError) {
|
||||
// 静默失败
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从编辑器创建行为树文件
|
||||
*/
|
||||
static async createFromEditor(data: { fileName: string, content: string }): Promise<void> {
|
||||
try {
|
||||
const projectPath = Editor.Project.path;
|
||||
const assetsPath = path.join(projectPath, 'assets');
|
||||
|
||||
let fileName = data.fileName;
|
||||
let counter = 1;
|
||||
let filePath = path.join(assetsPath, `${fileName}.bt.json`);
|
||||
|
||||
while (fs.existsSync(filePath)) {
|
||||
fileName = `${data.fileName}_${counter}`;
|
||||
filePath = path.join(assetsPath, `${fileName}.bt.json`);
|
||||
counter++;
|
||||
}
|
||||
|
||||
await fsExtra.writeFile(filePath, data.content);
|
||||
|
||||
const relativeAssetPath = path.relative(projectPath, filePath).replace(/\\/g, '/');
|
||||
const dbAssetPath = 'db://' + relativeAssetPath;
|
||||
await Editor.Message.request('asset-db', 'refresh-asset', dbAssetPath);
|
||||
|
||||
Editor.Dialog.info('保存成功', {
|
||||
detail: `行为树文件 "${fileName}.bt.json" 已保存到 assets 目录中!`,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
Editor.Dialog.error('保存失败', {
|
||||
detail: `保存行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 覆盖现有行为树文件
|
||||
*/
|
||||
static async overwriteFile(data: { filePath: string, content: string }): Promise<void> {
|
||||
try {
|
||||
await fsExtra.writeFile(data.filePath, data.content);
|
||||
|
||||
const projectPath = Editor.Project.path;
|
||||
const relativeAssetPath = path.relative(projectPath, data.filePath).replace(/\\/g, '/');
|
||||
const dbAssetPath = 'db://' + relativeAssetPath;
|
||||
await Editor.Message.request('asset-db', 'refresh-asset', dbAssetPath);
|
||||
|
||||
const fileName = path.basename(data.filePath, path.extname(data.filePath));
|
||||
Editor.Dialog.info('覆盖成功', {
|
||||
detail: `行为树文件 "${fileName}.bt.json" 已更新!`,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
Editor.Dialog.error('覆盖失败', {
|
||||
detail: `覆盖行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
import { exec } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { TemplateGenerator } from '../TemplateGenerator';
|
||||
|
||||
/**
|
||||
* ECS框架相关的处理器
|
||||
*/
|
||||
export class EcsFrameworkHandler {
|
||||
/**
|
||||
* 安装ECS Framework
|
||||
*/
|
||||
static async install(): Promise<void> {
|
||||
const projectPath = Editor.Project.path;
|
||||
const command = 'npm install @esengine/ecs-framework';
|
||||
|
||||
console.log(`Installing ECS Framework to project: ${projectPath}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
|
||||
console.log('Install stdout:', stdout);
|
||||
if (stderr) console.log('Install stderr:', stderr);
|
||||
|
||||
if (error) {
|
||||
console.error('Installation failed:', error);
|
||||
reject(error);
|
||||
} else {
|
||||
console.log('Installation completed successfully');
|
||||
|
||||
// 验证安装是否成功
|
||||
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
|
||||
const installSuccess = fs.existsSync(nodeModulesPath);
|
||||
|
||||
if (installSuccess) {
|
||||
console.log('ECS Framework installed successfully');
|
||||
resolve();
|
||||
} else {
|
||||
console.warn('ECS Framework directory not found after install');
|
||||
reject(new Error('安装验证失败'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新ECS Framework
|
||||
*/
|
||||
static async update(targetVersion?: string): Promise<void> {
|
||||
const projectPath = Editor.Project.path;
|
||||
const version = targetVersion ? `@${targetVersion}` : '@latest';
|
||||
const command = `npm install @esengine/ecs-framework${version}`;
|
||||
|
||||
console.log(`Updating ECS Framework to ${version} in project: ${projectPath}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
|
||||
console.log('Update stdout:', stdout);
|
||||
if (stderr) console.log('Update stderr:', stderr);
|
||||
|
||||
if (error) {
|
||||
console.error('Update failed:', error);
|
||||
reject(error);
|
||||
} else {
|
||||
console.log('Update completed successfully');
|
||||
|
||||
// 验证更新是否成功
|
||||
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
|
||||
const updateSuccess = fs.existsSync(nodeModulesPath);
|
||||
|
||||
if (updateSuccess) {
|
||||
console.log(`ECS Framework updated successfully to ${version}`);
|
||||
resolve();
|
||||
} else {
|
||||
console.warn('ECS Framework directory not found after update');
|
||||
reject(new Error('更新验证失败'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载ECS Framework
|
||||
*/
|
||||
static async uninstall(): Promise<void> {
|
||||
const projectPath = Editor.Project.path;
|
||||
const command = 'npm uninstall @esengine/ecs-framework';
|
||||
|
||||
console.log(`Uninstalling ECS Framework from project: ${projectPath}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, { cwd: projectPath }, (error, stdout, stderr) => {
|
||||
console.log('Uninstall stdout:', stdout);
|
||||
if (stderr) console.log('Uninstall stderr:', stderr);
|
||||
|
||||
if (error) {
|
||||
console.error('Uninstall failed:', error);
|
||||
reject(error);
|
||||
} else {
|
||||
console.log('Uninstall completed successfully');
|
||||
|
||||
// 检查是否真的卸载了
|
||||
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
|
||||
const stillExists = fs.existsSync(nodeModulesPath);
|
||||
|
||||
if (stillExists) {
|
||||
console.warn('ECS Framework directory still exists after uninstall');
|
||||
reject(new Error('卸载验证失败'));
|
||||
} else {
|
||||
console.log('ECS Framework uninstalled successfully');
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开文档
|
||||
*/
|
||||
static openDocumentation(): void {
|
||||
const url = 'https://github.com/esengine/ecs-framework/blob/master/README.md';
|
||||
|
||||
try {
|
||||
// 使用Electron的shell模块打开外部链接
|
||||
const { shell } = require('electron');
|
||||
shell.openExternal(url);
|
||||
console.log('Documentation link opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open documentation:', error);
|
||||
Editor.Dialog.info('打开文档', {
|
||||
detail: `请手动访问以下链接查看文档:\n\n${url}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建ECS模板
|
||||
*/
|
||||
static createTemplate(): void {
|
||||
const projectPath = Editor.Project.path;
|
||||
console.log(`Creating ECS template in project: ${projectPath}`);
|
||||
|
||||
try {
|
||||
const templateGenerator = new TemplateGenerator(projectPath);
|
||||
|
||||
// 检查是否已存在模板
|
||||
if (templateGenerator.checkTemplateExists()) {
|
||||
const existingFiles = templateGenerator.getExistingFiles();
|
||||
const fileList = existingFiles.length > 0 ? existingFiles.join('\n• ') : '未检测到具体文件';
|
||||
|
||||
Editor.Dialog.warn('模板已存在', {
|
||||
detail: `检测到已存在ECS模板,包含以下文件:\n\n• ${fileList}\n\n是否要覆盖现有模板?`,
|
||||
buttons: ['覆盖', '取消'],
|
||||
}).then((result: any) => {
|
||||
if (result.response === 0) {
|
||||
// 用户选择覆盖
|
||||
console.log('User chose to overwrite existing template');
|
||||
templateGenerator.removeExistingTemplate();
|
||||
templateGenerator.createTemplate();
|
||||
this.showTemplateCreatedDialog();
|
||||
} else {
|
||||
console.log('User cancelled template creation');
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建新模板
|
||||
templateGenerator.createTemplate();
|
||||
console.log('ECS template created successfully');
|
||||
this.showTemplateCreatedDialog();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to create ECS template:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
Editor.Dialog.error('模板创建失败', {
|
||||
detail: `创建ECS模板时发生错误:\n\n${errorMessage}\n\n请检查项目权限和目录结构。`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示模板创建成功的对话框
|
||||
*/
|
||||
private static showTemplateCreatedDialog(): void {
|
||||
Editor.Dialog.info('模板创建成功', {
|
||||
detail: '✅ ECS项目模板已创建完成!\n\n已为您的Cocos Creator项目生成了完整的ECS架构模板,包括:\n\n' +
|
||||
'• 位置、速度、Cocos节点组件\n' +
|
||||
'• 移动系统和节点同步系统\n' +
|
||||
'• 实体工厂和场景管理器\n' +
|
||||
'• ECS管理器组件(可直接添加到节点)\n' +
|
||||
'• 完整的使用文档\n\n' +
|
||||
'请刷新资源管理器查看新创建的文件。',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开GitHub仓库
|
||||
*/
|
||||
static openGitHub(): void {
|
||||
const url = 'https://github.com/esengine/ecs-framework';
|
||||
|
||||
try {
|
||||
const { shell } = require('electron');
|
||||
shell.openExternal(url);
|
||||
console.log('GitHub repository opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open GitHub repository:', error);
|
||||
Editor.Dialog.info('打开GitHub', {
|
||||
detail: `请手动访问以下链接:\n\n${url}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开QQ群
|
||||
*/
|
||||
static openQQGroup(): void {
|
||||
const url = 'https://qm.qq.com/cgi-bin/qm/qr?k=your-qq-group-key';
|
||||
|
||||
try {
|
||||
const { shell } = require('electron');
|
||||
shell.openExternal(url);
|
||||
console.log('QQ group opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open QQ group:', error);
|
||||
Editor.Dialog.info('QQ群', {
|
||||
detail: '请手动搜索QQ群号或访问相关链接加入讨论群。',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,571 +0,0 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
/**
|
||||
* 热更新配置接口
|
||||
*/
|
||||
interface HotUpdateConfig {
|
||||
serverUrl: string;
|
||||
currentVersion: string;
|
||||
updateChannel: 'stable' | 'beta' | 'dev';
|
||||
autoCheck: boolean;
|
||||
checkInterval: number; // 分钟
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本信息接口
|
||||
*/
|
||||
interface VersionInfo {
|
||||
version: string;
|
||||
releaseDate: string;
|
||||
description: string;
|
||||
downloadUrl: string;
|
||||
fileSize: number;
|
||||
checksum: string;
|
||||
mandatory: boolean; // 是否强制更新
|
||||
files: UpdateFile[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文件接口
|
||||
*/
|
||||
interface UpdateFile {
|
||||
path: string;
|
||||
hash: string;
|
||||
size: number;
|
||||
action: 'add' | 'update' | 'delete';
|
||||
}
|
||||
|
||||
/**
|
||||
* 热更新处理器
|
||||
*/
|
||||
export class HotUpdateHandler {
|
||||
private static readonly CONFIG_FILE = 'hot-update-config.json';
|
||||
private static readonly VERSION_FILE = 'version-info.json';
|
||||
private static readonly EXTENSION_PATH = Editor.Package.getPath('cocos-ecs-extension') || '';
|
||||
|
||||
private static config: HotUpdateConfig;
|
||||
private static updateTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
/**
|
||||
* 初始化热更新系统
|
||||
*/
|
||||
static async initialize(): Promise<void> {
|
||||
console.log('[HotUpdate] 初始化热更新系统...');
|
||||
|
||||
try {
|
||||
await this.loadConfig();
|
||||
await this.startAutoCheck();
|
||||
console.log('[HotUpdate] 热更新系统初始化完成');
|
||||
} catch (error) {
|
||||
console.error('[HotUpdate] 初始化失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载配置
|
||||
*/
|
||||
private static async loadConfig(): Promise<void> {
|
||||
const configPath = path.join(this.EXTENSION_PATH, this.CONFIG_FILE);
|
||||
|
||||
try {
|
||||
if (await fs.pathExists(configPath)) {
|
||||
this.config = await fs.readJSON(configPath);
|
||||
} else {
|
||||
// 创建默认配置
|
||||
this.config = {
|
||||
serverUrl: 'https://earthonline-game.cn/api/plugin-updates',
|
||||
currentVersion: this.getCurrentVersion(),
|
||||
updateChannel: 'stable',
|
||||
autoCheck: true,
|
||||
checkInterval: 60 // 60分钟检查一次
|
||||
};
|
||||
await this.saveConfig();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[HotUpdate] 配置加载失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置
|
||||
*/
|
||||
private static async saveConfig(): Promise<void> {
|
||||
const configPath = path.join(this.EXTENSION_PATH, this.CONFIG_FILE);
|
||||
await fs.writeJSON(configPath, this.config, { spaces: 2 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前版本
|
||||
*/
|
||||
private static getCurrentVersion(): string {
|
||||
try {
|
||||
const packagePath = path.join(this.EXTENSION_PATH, 'package.json');
|
||||
const packageInfo = fs.readJSONSync(packagePath);
|
||||
return packageInfo.version;
|
||||
} catch (error) {
|
||||
console.error('[HotUpdate] 无法获取当前版本:', error);
|
||||
return '1.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始自动检查
|
||||
*/
|
||||
private static async startAutoCheck(): Promise<void> {
|
||||
if (!this.config.autoCheck) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 立即检查一次
|
||||
await this.checkForUpdates(true);
|
||||
|
||||
// 设置定时检查
|
||||
if (this.updateTimer) {
|
||||
clearInterval(this.updateTimer);
|
||||
}
|
||||
|
||||
this.updateTimer = setInterval(async () => {
|
||||
await this.checkForUpdates(true);
|
||||
}, this.config.checkInterval * 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
*/
|
||||
static async checkForUpdates(silent: boolean = false): Promise<VersionInfo | null> {
|
||||
console.log('[HotUpdate] 检查更新中...');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.config.serverUrl}/check`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
currentVersion: this.config.currentVersion,
|
||||
pluginId: 'cocos-ecs-extension', // 当前插件ID
|
||||
channel: this.config.updateChannel,
|
||||
platform: process.platform,
|
||||
editorVersion: Editor.App.version
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`服务器响应错误: ${response.status}`);
|
||||
}
|
||||
|
||||
const versionInfo: VersionInfo = await response.json();
|
||||
|
||||
if (this.isNewerVersion(versionInfo.version, this.config.currentVersion)) {
|
||||
console.log(`[HotUpdate] 发现新版本: ${versionInfo.version}`);
|
||||
|
||||
if (!silent) {
|
||||
await this.showUpdateDialog(versionInfo);
|
||||
}
|
||||
|
||||
return versionInfo;
|
||||
} else {
|
||||
if (!silent) {
|
||||
Editor.Dialog.info('检查更新', {
|
||||
detail: '当前已是最新版本!'
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[HotUpdate] 检查更新失败:', error);
|
||||
|
||||
if (!silent) {
|
||||
Editor.Dialog.error('检查更新失败', {
|
||||
detail: `无法连接到更新服务器:\n\n${error instanceof Error ? error.message : String(error)}`
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较版本号
|
||||
*/
|
||||
private static isNewerVersion(newVersion: string, currentVersion: string): boolean {
|
||||
const parseVersion = (version: string) => {
|
||||
return version.split('.').map(Number);
|
||||
};
|
||||
|
||||
const newParts = parseVersion(newVersion);
|
||||
const currentParts = parseVersion(currentVersion);
|
||||
const maxLength = Math.max(newParts.length, currentParts.length);
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const newPart = newParts[i] || 0;
|
||||
const currentPart = currentParts[i] || 0;
|
||||
|
||||
if (newPart > currentPart) return true;
|
||||
if (newPart < currentPart) return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示更新对话框
|
||||
*/
|
||||
private static async showUpdateDialog(versionInfo: VersionInfo): Promise<void> {
|
||||
const message = `发现新版本 ${versionInfo.version}!\n\n` +
|
||||
`发布时间: ${versionInfo.releaseDate}\n\n` +
|
||||
`更新内容:\n${versionInfo.description}\n\n` +
|
||||
`文件大小: ${this.formatFileSize(versionInfo.fileSize)}`;
|
||||
|
||||
const buttons = versionInfo.mandatory ? ['立即更新'] : ['立即更新', '稍后提醒', '跳过此版本'];
|
||||
|
||||
const result = await Editor.Dialog.info('插件更新', {
|
||||
detail: message,
|
||||
buttons: buttons
|
||||
});
|
||||
|
||||
switch (result.response) {
|
||||
case 0: // 立即更新
|
||||
await this.downloadAndInstallUpdate(versionInfo);
|
||||
break;
|
||||
case 1: // 稍后提醒
|
||||
if (!versionInfo.mandatory) {
|
||||
console.log('[HotUpdate] 用户选择稍后更新');
|
||||
}
|
||||
break;
|
||||
case 2: // 跳过此版本
|
||||
if (!versionInfo.mandatory) {
|
||||
await this.skipVersion(versionInfo.version);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载并安装更新
|
||||
*/
|
||||
private static async downloadAndInstallUpdate(versionInfo: VersionInfo): Promise<void> {
|
||||
console.log(`[HotUpdate] 开始下载更新: ${versionInfo.version}`);
|
||||
|
||||
try {
|
||||
// 显示进度对话框
|
||||
const progressDialog = this.showProgressDialog('正在下载更新...');
|
||||
|
||||
// 下载更新包
|
||||
const updatePath = await this.downloadUpdate(versionInfo, (progress) => {
|
||||
// 更新进度
|
||||
console.log(`[HotUpdate] 下载进度: ${progress}%`);
|
||||
});
|
||||
|
||||
progressDialog.detail = '正在验证文件...';
|
||||
|
||||
// 验证文件完整性
|
||||
const isValid = await this.verifyUpdate(updatePath, versionInfo.checksum);
|
||||
if (!isValid) {
|
||||
throw new Error('文件校验失败,更新包可能已损坏');
|
||||
}
|
||||
|
||||
progressDialog.detail = '正在安装更新...';
|
||||
|
||||
// 安装更新
|
||||
await this.installUpdate(updatePath, versionInfo);
|
||||
|
||||
// 更新版本信息
|
||||
this.config.currentVersion = versionInfo.version;
|
||||
await this.saveConfig();
|
||||
|
||||
// 显示安装完成对话框
|
||||
const result = await Editor.Dialog.info('更新完成', {
|
||||
detail: `插件已成功更新到版本 ${versionInfo.version}!\n\n为了使更新生效,需要重启Cocos Creator编辑器。`,
|
||||
buttons: ['立即重启', '稍后重启']
|
||||
});
|
||||
|
||||
if (result.response === 0) {
|
||||
this.restartEditor();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[HotUpdate] 更新失败:', error);
|
||||
Editor.Dialog.error('更新失败', {
|
||||
detail: `更新过程中发生错误:\n\n${error instanceof Error ? error.message : String(error)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载更新
|
||||
*/
|
||||
private static async downloadUpdate(versionInfo: VersionInfo, onProgress?: (progress: number) => void): Promise<string> {
|
||||
const response = await fetch(versionInfo.downloadUrl);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载失败: ${response.status}`);
|
||||
}
|
||||
|
||||
const totalSize = parseInt(response.headers.get('content-length') || '0');
|
||||
let downloadedSize = 0;
|
||||
|
||||
const tempPath = path.join(this.EXTENSION_PATH, 'temp', `update-${versionInfo.version}.zip`);
|
||||
await fs.ensureDir(path.dirname(tempPath));
|
||||
|
||||
const writer = fs.createWriteStream(tempPath);
|
||||
const reader = response.body?.getReader();
|
||||
|
||||
if (!reader) {
|
||||
throw new Error('无法创建下载流');
|
||||
}
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) break;
|
||||
|
||||
writer.write(value);
|
||||
downloadedSize += value.length;
|
||||
|
||||
if (onProgress && totalSize > 0) {
|
||||
const progress = Math.round((downloadedSize / totalSize) * 100);
|
||||
onProgress(progress);
|
||||
}
|
||||
}
|
||||
|
||||
writer.end();
|
||||
return tempPath;
|
||||
|
||||
} catch (error) {
|
||||
writer.destroy();
|
||||
await fs.remove(tempPath).catch(() => {}); // 忽略删除错误
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证更新包
|
||||
*/
|
||||
private static async verifyUpdate(filePath: string, expectedChecksum: string): Promise<boolean> {
|
||||
try {
|
||||
const fileBuffer = await fs.readFile(filePath);
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(fileBuffer);
|
||||
const actualChecksum = hash.digest('hex');
|
||||
|
||||
return actualChecksum === expectedChecksum;
|
||||
} catch (error) {
|
||||
console.error('[HotUpdate] 文件校验失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装更新
|
||||
*/
|
||||
private static async installUpdate(updatePath: string, versionInfo: VersionInfo): Promise<void> {
|
||||
const extractPath = path.join(this.EXTENSION_PATH, 'temp', 'extract');
|
||||
|
||||
try {
|
||||
// 清理临时目录
|
||||
await fs.remove(extractPath);
|
||||
await fs.ensureDir(extractPath);
|
||||
|
||||
// 解压更新包
|
||||
await this.extractZip(updatePath, extractPath);
|
||||
|
||||
// 备份当前版本
|
||||
const backupPath = path.join(this.EXTENSION_PATH, 'backup', this.config.currentVersion);
|
||||
await this.createBackup(backupPath);
|
||||
|
||||
// 应用更新文件
|
||||
await this.applyUpdateFiles(extractPath, versionInfo.files);
|
||||
|
||||
// 清理临时文件
|
||||
await fs.remove(path.dirname(updatePath));
|
||||
|
||||
} catch (error) {
|
||||
console.error('[HotUpdate] 安装更新失败:', error);
|
||||
// 尝试恢复备份
|
||||
await this.restoreBackup();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压ZIP文件
|
||||
*/
|
||||
private static async extractZip(zipPath: string, extractPath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 这里使用node的解压库,您可能需要安装 yauzl 或 adm-zip
|
||||
const AdmZip = require('adm-zip');
|
||||
|
||||
try {
|
||||
const zip = new AdmZip(zipPath);
|
||||
zip.extractAllTo(extractPath, true);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用更新文件
|
||||
*/
|
||||
private static async applyUpdateFiles(extractPath: string, files: UpdateFile[]): Promise<void> {
|
||||
for (const file of files) {
|
||||
const sourcePath = path.join(extractPath, file.path);
|
||||
const targetPath = path.join(this.EXTENSION_PATH, file.path);
|
||||
|
||||
try {
|
||||
switch (file.action) {
|
||||
case 'add':
|
||||
case 'update':
|
||||
await fs.ensureDir(path.dirname(targetPath));
|
||||
await fs.copy(sourcePath, targetPath, { overwrite: true });
|
||||
break;
|
||||
case 'delete':
|
||||
await fs.remove(targetPath);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[HotUpdate] 处理文件失败 ${file.path}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建备份
|
||||
*/
|
||||
private static async createBackup(backupPath: string): Promise<void> {
|
||||
await fs.ensureDir(backupPath);
|
||||
|
||||
const sourceFiles = [
|
||||
'source',
|
||||
'static',
|
||||
'package.json',
|
||||
'README.md'
|
||||
];
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
const sourcePath = path.join(this.EXTENSION_PATH, file);
|
||||
const targetPath = path.join(backupPath, file);
|
||||
|
||||
if (await fs.pathExists(sourcePath)) {
|
||||
await fs.copy(sourcePath, targetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复备份
|
||||
*/
|
||||
private static async restoreBackup(): Promise<void> {
|
||||
const backupPath = path.join(this.EXTENSION_PATH, 'backup', this.config.currentVersion);
|
||||
|
||||
if (await fs.pathExists(backupPath)) {
|
||||
console.log('[HotUpdate] 正在恢复备份...');
|
||||
|
||||
const backupFiles = await fs.readdir(backupPath);
|
||||
for (const file of backupFiles) {
|
||||
const sourcePath = path.join(backupPath, file);
|
||||
const targetPath = path.join(this.EXTENSION_PATH, file);
|
||||
|
||||
await fs.copy(sourcePath, targetPath, { overwrite: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳过版本
|
||||
*/
|
||||
private static async skipVersion(version: string): Promise<void> {
|
||||
const skipPath = path.join(this.EXTENSION_PATH, 'skipped-versions.json');
|
||||
let skippedVersions: string[] = [];
|
||||
|
||||
if (await fs.pathExists(skipPath)) {
|
||||
skippedVersions = await fs.readJSON(skipPath);
|
||||
}
|
||||
|
||||
if (!skippedVersions.includes(version)) {
|
||||
skippedVersions.push(version);
|
||||
await fs.writeJSON(skipPath, skippedVersions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示进度对话框
|
||||
*/
|
||||
private static showProgressDialog(message: string) {
|
||||
// 这是一个简化版本,实际可能需要创建自定义进度条
|
||||
return {
|
||||
detail: message
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
private static formatFileSize(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重启编辑器
|
||||
*/
|
||||
private static restartEditor(): void {
|
||||
// 注意:这个功能需要特殊权限,可能需要用户手动重启
|
||||
console.log('[HotUpdate] 请求重启编辑器');
|
||||
|
||||
try {
|
||||
// 尝试重启编辑器(可能不会成功)
|
||||
Editor.App.quit();
|
||||
} catch (error) {
|
||||
console.warn('[HotUpdate] 无法自动重启编辑器:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置更新配置
|
||||
*/
|
||||
static async setConfig(newConfig: Partial<HotUpdateConfig>): Promise<void> {
|
||||
this.config = { ...this.config, ...newConfig };
|
||||
await this.saveConfig();
|
||||
|
||||
// 重新启动自动检查
|
||||
if (this.config.autoCheck) {
|
||||
await this.startAutoCheck();
|
||||
} else if (this.updateTimer) {
|
||||
clearInterval(this.updateTimer);
|
||||
this.updateTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
*/
|
||||
static getConfig(): HotUpdateConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
static cleanup(): void {
|
||||
if (this.updateTimer) {
|
||||
clearInterval(this.updateTimer);
|
||||
this.updateTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* 面板管理相关的处理器
|
||||
*/
|
||||
export class PanelHandler {
|
||||
/**
|
||||
* 打开默认面板
|
||||
*/
|
||||
static openDefaultPanel(): void {
|
||||
try {
|
||||
Editor.Panel.open('cocos-ecs-extension');
|
||||
console.log('Default panel opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open default panel:', error);
|
||||
Editor.Dialog.error('打开面板失败', {
|
||||
detail: `无法打开面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开调试面板
|
||||
*/
|
||||
static openDebugPanel(): void {
|
||||
try {
|
||||
Editor.Panel.open('cocos-ecs-extension.debug');
|
||||
console.log('Debug panel opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open debug panel:', error);
|
||||
Editor.Dialog.error('打开调试面板失败', {
|
||||
detail: `无法打开调试面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开代码生成器面板
|
||||
*/
|
||||
static openGeneratorPanel(): void {
|
||||
try {
|
||||
Editor.Panel.open('cocos-ecs-extension.generator');
|
||||
console.log('Generator panel opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open generator panel:', error);
|
||||
Editor.Dialog.error('打开代码生成器失败', {
|
||||
detail: `无法打开代码生成器面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开行为树面板
|
||||
*/
|
||||
static openBehaviorTreePanel(): void {
|
||||
try {
|
||||
Editor.Panel.open('cocos-ecs-extension.behavior-tree');
|
||||
console.log('Behavior Tree panel opened successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to open behavior tree panel:', error);
|
||||
Editor.Dialog.error('打开行为树面板失败', {
|
||||
detail: `无法打开行为树AI组件库面板:\n\n${error}\n\n请尝试重启Cocos Creator编辑器。`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export { EcsFrameworkHandler } from './EcsFrameworkHandler';
|
||||
export { BehaviorTreeHandler } from './BehaviorTreeHandler';
|
||||
export { PanelHandler } from './PanelHandler';
|
||||
export { HotUpdateHandler } from './HotUpdateHandler';
|
||||
@@ -1,229 +0,0 @@
|
||||
// @ts-ignore
|
||||
import packageJSON from '../package.json';
|
||||
import { EcsFrameworkHandler, BehaviorTreeHandler, PanelHandler, HotUpdateHandler } from './handlers';
|
||||
import { readJSON } from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { AssetInfo } from '@cocos/creator-types/editor/packages/asset-db/@types/public';
|
||||
|
||||
/**
|
||||
* @en Registration method for the main process of Extension
|
||||
* @zh 为扩展的主进程的注册方法
|
||||
*/
|
||||
export const methods: { [key: string]: (...any: any) => any } = {
|
||||
// ================ 面板管理 ================
|
||||
/**
|
||||
* 打开默认面板
|
||||
*/
|
||||
openPanel() {
|
||||
PanelHandler.openDefaultPanel();
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开调试面板
|
||||
*/
|
||||
'open-debug'() {
|
||||
PanelHandler.openDebugPanel();
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开代码生成器面板
|
||||
*/
|
||||
'open-generator'() {
|
||||
PanelHandler.openGeneratorPanel();
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开行为树面板
|
||||
*/
|
||||
'open-behavior-tree'() {
|
||||
PanelHandler.openBehaviorTreePanel();
|
||||
},
|
||||
|
||||
// ================ ECS框架管理 ================
|
||||
/**
|
||||
* 安装ECS Framework
|
||||
*/
|
||||
'install-ecs-framework'() {
|
||||
EcsFrameworkHandler.install();
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新ECS Framework
|
||||
*/
|
||||
'update-ecs-framework'() {
|
||||
EcsFrameworkHandler.update();
|
||||
},
|
||||
|
||||
/**
|
||||
* 卸载ECS Framework
|
||||
*/
|
||||
'uninstall-ecs-framework'() {
|
||||
EcsFrameworkHandler.uninstall();
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开文档
|
||||
*/
|
||||
'open-documentation'() {
|
||||
EcsFrameworkHandler.openDocumentation();
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建ECS模板
|
||||
*/
|
||||
'create-ecs-template'() {
|
||||
EcsFrameworkHandler.createTemplate();
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开GitHub仓库
|
||||
*/
|
||||
'open-github'() {
|
||||
EcsFrameworkHandler.openGitHub();
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开QQ群
|
||||
*/
|
||||
'open-qq-group'() {
|
||||
EcsFrameworkHandler.openQQGroup();
|
||||
},
|
||||
|
||||
// ================ 行为树管理 ================
|
||||
/**
|
||||
* 安装行为树AI系统
|
||||
*/
|
||||
async 'install-behavior-tree'() {
|
||||
try {
|
||||
return await BehaviorTreeHandler.install();
|
||||
} catch (error) {
|
||||
console.error('安装行为树AI系统失败:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新行为树AI系统
|
||||
*/
|
||||
async 'update-behavior-tree'() {
|
||||
try {
|
||||
return await BehaviorTreeHandler.update();
|
||||
} catch (error) {
|
||||
console.error('更新行为树AI系统失败:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查行为树AI是否已安装
|
||||
*/
|
||||
'check-behavior-tree-installed'() {
|
||||
return BehaviorTreeHandler.checkInstalled();
|
||||
},
|
||||
|
||||
/**
|
||||
* 打开行为树文档
|
||||
*/
|
||||
'open-behavior-tree-docs'() {
|
||||
BehaviorTreeHandler.openDocumentation();
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建行为树文件
|
||||
*/
|
||||
'create-behavior-tree-file'() {
|
||||
BehaviorTreeHandler.createFile();
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载行为树文件到编辑器
|
||||
*/
|
||||
async 'load-behavior-tree-file'(...args: any[]) {
|
||||
const assetInfo = args.length >= 2 ? args[1] : args[0];
|
||||
|
||||
try {
|
||||
if (!assetInfo || (!assetInfo.file && !assetInfo.path)) {
|
||||
throw new Error('无效的文件信息');
|
||||
}
|
||||
|
||||
await Editor.Panel.open('cocos-ecs-extension.behavior-tree');
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
const result = await Editor.Message.request('cocos-ecs-extension', 'behavior-tree-panel-load-file', assetInfo);
|
||||
|
||||
} catch (error) {
|
||||
Editor.Dialog.error('打开失败', {
|
||||
detail: `打开行为树文件失败:\n\n${error instanceof Error ? error.message : String(error)}`
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 从编辑器创建行为树文件
|
||||
*/
|
||||
'create-behavior-tree-from-editor'(event: any, data: any) {
|
||||
BehaviorTreeHandler.createFromEditor(data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 覆盖现有行为树文件
|
||||
*/
|
||||
'overwrite-behavior-tree-file'(...args: any[]) {
|
||||
const data = args.length >= 2 ? args[1] : args[0];
|
||||
|
||||
if (data && data.filePath) {
|
||||
BehaviorTreeHandler.overwriteFile(data);
|
||||
} else {
|
||||
throw new Error('文件路径不存在或数据无效');
|
||||
}
|
||||
},
|
||||
|
||||
// ================ 热更新管理 ================
|
||||
/**
|
||||
* 检查插件更新
|
||||
*/
|
||||
'check-plugin-updates'() {
|
||||
return HotUpdateHandler.checkForUpdates(false);
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置热更新配置
|
||||
*/
|
||||
'set-hot-update-config'(...args: any[]) {
|
||||
const config = args.length >= 2 ? args[1] : args[0];
|
||||
return HotUpdateHandler.setConfig(config);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取热更新配置
|
||||
*/
|
||||
'get-hot-update-config'() {
|
||||
return HotUpdateHandler.getConfig();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @en Method triggered when the extension is started
|
||||
* @zh 启动扩展时触发的方法
|
||||
*/
|
||||
export function load() {
|
||||
console.log('[Cocos ECS Extension] 扩展已加载');
|
||||
|
||||
// 初始化热更新系统
|
||||
HotUpdateHandler.initialize().catch(error => {
|
||||
console.error('[Cocos ECS Extension] 热更新初始化失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @en Method triggered when the extension is uninstalled
|
||||
* @zh 卸载扩展时触发的方法
|
||||
*/
|
||||
export function unload() {
|
||||
console.log('[Cocos ECS Extension] 扩展已卸载');
|
||||
|
||||
// 清理热更新资源
|
||||
HotUpdateHandler.cleanup();
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import { ref } from 'vue';
|
||||
import { TreeNode, DragState, Connection } from '../types';
|
||||
import { allNodeTemplates as nodeTemplates } from '../data/nodeTemplates';
|
||||
|
||||
/**
|
||||
* 应用状态管理
|
||||
*/
|
||||
export function useAppState() {
|
||||
// 安装状态
|
||||
const checkingStatus = ref(true);
|
||||
const isInstalled = ref(false);
|
||||
const version = ref<string | null>(null);
|
||||
const isInstalling = ref(false);
|
||||
|
||||
// 编辑器状态
|
||||
const nodeTemplates_ = ref(nodeTemplates);
|
||||
const treeNodes = ref<TreeNode[]>([]);
|
||||
const selectedNodeId = ref<string | null>(null);
|
||||
const selectedConditionNodeId = ref<string | null>(null); // 选中的条件节点ID
|
||||
const nodeSearchText = ref('');
|
||||
|
||||
// 调试:检查条件节点模板
|
||||
console.log('🔍 条件节点模板检查:');
|
||||
nodeTemplates.filter(t => t.category === 'condition').forEach(template => {
|
||||
console.log(` ${template.name}: isDraggableCondition=${template.isDraggableCondition}`);
|
||||
});
|
||||
|
||||
console.log('🎭 装饰器节点模板检查:');
|
||||
nodeTemplates.filter(t => t.category === 'decorator').forEach(template => {
|
||||
console.log(` ${template.name}: type=${template.type}`);
|
||||
});
|
||||
|
||||
// 画布状态
|
||||
const canvasWidth = ref(800);
|
||||
const canvasHeight = ref(600);
|
||||
const zoomLevel = ref(1);
|
||||
const panX = ref(0);
|
||||
const panY = ref(0);
|
||||
|
||||
const dragState = ref<DragState>({
|
||||
isDraggingCanvas: false,
|
||||
isDraggingNode: false,
|
||||
isConnecting: false,
|
||||
dragStartX: 0,
|
||||
dragStartY: 0,
|
||||
dragNodeId: null,
|
||||
dragNodeStartX: 0,
|
||||
dragNodeStartY: 0,
|
||||
connectionStart: null,
|
||||
connectionEnd: { x: 0, y: 0 }
|
||||
});
|
||||
|
||||
// 连接状态
|
||||
const connections = ref<Connection[]>([]);
|
||||
const tempConnection = ref({ path: '' });
|
||||
|
||||
// UI状态
|
||||
const showExportModal = ref(false);
|
||||
const exportFormat = ref('json'); // 默认JSON格式,TypeScript暂时禁用
|
||||
|
||||
// 工具函数
|
||||
const getNodeByIdLocal = (id: string): TreeNode | undefined => {
|
||||
return treeNodes.value.find(node => node.id === id);
|
||||
};
|
||||
|
||||
const selectNode = (nodeId: string) => {
|
||||
selectedNodeId.value = nodeId;
|
||||
};
|
||||
|
||||
const newBehaviorTree = () => {
|
||||
treeNodes.value = [];
|
||||
selectedNodeId.value = null;
|
||||
connections.value = [];
|
||||
tempConnection.value.path = '';
|
||||
};
|
||||
|
||||
const updateCanvasSize = () => {
|
||||
const canvasArea = document.querySelector('.canvas-area') as HTMLElement;
|
||||
if (canvasArea) {
|
||||
const rect = canvasArea.getBoundingClientRect();
|
||||
if (rect.width > 0 && rect.height > 0) {
|
||||
canvasWidth.value = Math.max(rect.width, 800);
|
||||
canvasHeight.value = Math.max(rect.height, 600);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// 安装状态
|
||||
checkingStatus,
|
||||
isInstalled,
|
||||
version,
|
||||
isInstalling,
|
||||
|
||||
// 编辑器状态
|
||||
nodeTemplates: nodeTemplates_,
|
||||
treeNodes,
|
||||
selectedNodeId,
|
||||
selectedConditionNodeId,
|
||||
nodeSearchText,
|
||||
|
||||
// 画布状态
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
zoomLevel,
|
||||
panX,
|
||||
panY,
|
||||
dragState,
|
||||
|
||||
// 连接状态
|
||||
connections,
|
||||
tempConnection,
|
||||
|
||||
// UI状态
|
||||
showExportModal,
|
||||
exportFormat,
|
||||
|
||||
// 工具函数
|
||||
getNodeByIdLocal,
|
||||
selectNode,
|
||||
newBehaviorTree,
|
||||
updateCanvasSize
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,474 +0,0 @@
|
||||
import { ref, computed, reactive } from 'vue';
|
||||
|
||||
export interface BlackboardVariable {
|
||||
name: string;
|
||||
type: 'string' | 'number' | 'boolean' | 'vector2' | 'vector3' | 'object' | 'array';
|
||||
value: any;
|
||||
defaultValue: any;
|
||||
description?: string;
|
||||
group?: string;
|
||||
readOnly?: boolean;
|
||||
constraints?: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
allowedValues?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface BlackboardModalData {
|
||||
name: string;
|
||||
type: 'string' | 'number' | 'boolean' | 'vector2' | 'vector3' | 'object' | 'array';
|
||||
defaultValue: any;
|
||||
description: string;
|
||||
group: string;
|
||||
readOnly: boolean;
|
||||
constraints: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
};
|
||||
useAllowedValues: boolean;
|
||||
allowedValuesText: string;
|
||||
}
|
||||
|
||||
export function useBlackboard() {
|
||||
const blackboardVariables = ref<Map<string, BlackboardVariable>>(new Map());
|
||||
const expandedGroups = ref<Set<string>>(new Set(['未分组']));
|
||||
const selectedVariable = ref<BlackboardVariable | null>(null);
|
||||
const showBlackboardModal = ref(false);
|
||||
const editingBlackboardVariable = ref<BlackboardVariable | null>(null);
|
||||
|
||||
const blackboardModalData = reactive<BlackboardModalData>({
|
||||
name: '',
|
||||
type: 'string',
|
||||
defaultValue: '',
|
||||
description: '',
|
||||
group: '',
|
||||
readOnly: false,
|
||||
constraints: {},
|
||||
useAllowedValues: false,
|
||||
allowedValuesText: ''
|
||||
});
|
||||
|
||||
const blackboardCollapsed = ref(false);
|
||||
const blackboardTransparent = ref(true);
|
||||
|
||||
const blackboardVariablesArray = computed(() => {
|
||||
return Array.from(blackboardVariables.value.values());
|
||||
});
|
||||
|
||||
const blackboardVariableGroups = computed(() => {
|
||||
const groups: Record<string, BlackboardVariable[]> = {};
|
||||
|
||||
blackboardVariables.value.forEach(variable => {
|
||||
const groupName = variable.group || '未分组';
|
||||
if (!groups[groupName]) {
|
||||
groups[groupName] = [];
|
||||
}
|
||||
groups[groupName].push(variable);
|
||||
});
|
||||
|
||||
const sortedGroups: Record<string, BlackboardVariable[]> = {};
|
||||
const groupNames = Object.keys(groups).sort((a, b) => {
|
||||
if (a === '未分组') return -1;
|
||||
if (b === '未分组') return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
groupNames.forEach(groupName => {
|
||||
groups[groupName].sort((a, b) => a.name.localeCompare(b.name));
|
||||
sortedGroups[groupName] = groups[groupName];
|
||||
});
|
||||
|
||||
return sortedGroups;
|
||||
});
|
||||
|
||||
const groupedBlackboardVariables = () => {
|
||||
return Object.entries(blackboardVariableGroups.value);
|
||||
};
|
||||
|
||||
const isGroupExpanded = (groupName: string): boolean => {
|
||||
return expandedGroups.value.has(groupName);
|
||||
};
|
||||
|
||||
const toggleGroup = (groupName: string) => {
|
||||
if (expandedGroups.value.has(groupName)) {
|
||||
expandedGroups.value.delete(groupName);
|
||||
} else {
|
||||
expandedGroups.value.add(groupName);
|
||||
}
|
||||
};
|
||||
|
||||
const getVariableTypeIcon = (type: string): string => {
|
||||
const iconMap: Record<string, string> = {
|
||||
string: '📝',
|
||||
number: '🔢',
|
||||
boolean: '☑️',
|
||||
vector2: '📐',
|
||||
vector3: '🧊',
|
||||
object: '📦',
|
||||
array: '📋'
|
||||
};
|
||||
return iconMap[type] || '❓';
|
||||
};
|
||||
|
||||
const formatBlackboardValue = (variable: BlackboardVariable): string => {
|
||||
if (variable.value === null || variable.value === undefined) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
switch (variable.type) {
|
||||
case 'boolean':
|
||||
return variable.value ? 'true' : 'false';
|
||||
case 'string':
|
||||
return `"${variable.value}"`;
|
||||
case 'number':
|
||||
return variable.value.toString();
|
||||
default:
|
||||
return String(variable.value);
|
||||
}
|
||||
};
|
||||
|
||||
const hasVisibleConstraints = (variable: BlackboardVariable): boolean => {
|
||||
if (!variable.constraints) return false;
|
||||
|
||||
return !!(
|
||||
variable.constraints.min !== undefined ||
|
||||
variable.constraints.max !== undefined ||
|
||||
variable.constraints.allowedValues?.length
|
||||
);
|
||||
};
|
||||
|
||||
const formatConstraints = (constraints: BlackboardVariable['constraints']): string => {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (constraints?.min !== undefined) {
|
||||
parts.push(`最小: ${constraints.min}`);
|
||||
}
|
||||
if (constraints?.max !== undefined) {
|
||||
parts.push(`最大: ${constraints.max}`);
|
||||
}
|
||||
if (constraints?.allowedValues?.length) {
|
||||
parts.push(`可选: ${constraints.allowedValues.join(', ')}`);
|
||||
}
|
||||
|
||||
return parts.join(', ');
|
||||
};
|
||||
|
||||
const getTypeDisplayName = (type: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
string: 'STR',
|
||||
number: 'NUM',
|
||||
boolean: 'BOOL',
|
||||
vector2: 'VEC2',
|
||||
vector3: 'VEC3',
|
||||
object: 'OBJ',
|
||||
array: 'ARR'
|
||||
};
|
||||
return typeMap[type] || type.toUpperCase();
|
||||
};
|
||||
|
||||
const getDisplayValue = (variable: BlackboardVariable): string => {
|
||||
if (variable.value === null || variable.value === undefined) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
switch (variable.type) {
|
||||
case 'string':
|
||||
return String(variable.value);
|
||||
case 'number':
|
||||
return variable.value.toString();
|
||||
case 'boolean':
|
||||
return variable.value ? 'true' : 'false';
|
||||
case 'vector2':
|
||||
if (typeof variable.value === 'object' && variable.value.x !== undefined && variable.value.y !== undefined) {
|
||||
return `(${variable.value.x}, ${variable.value.y})`;
|
||||
}
|
||||
return String(variable.value);
|
||||
case 'vector3':
|
||||
if (typeof variable.value === 'object' && variable.value.x !== undefined && variable.value.y !== undefined && variable.value.z !== undefined) {
|
||||
return `(${variable.value.x}, ${variable.value.y}, ${variable.value.z})`;
|
||||
}
|
||||
return String(variable.value);
|
||||
case 'object':
|
||||
case 'array':
|
||||
try {
|
||||
const jsonStr = JSON.stringify(variable.value);
|
||||
return jsonStr.length > 20 ? jsonStr.substring(0, 17) + '...' : jsonStr;
|
||||
} catch {
|
||||
return String(variable.value);
|
||||
}
|
||||
default:
|
||||
return String(variable.value);
|
||||
}
|
||||
};
|
||||
|
||||
const saveBlackboardVariable = () => {
|
||||
if (!blackboardModalData.name.trim()) {
|
||||
alert('请输入变量名称');
|
||||
return;
|
||||
}
|
||||
|
||||
let finalValue = blackboardModalData.defaultValue;
|
||||
|
||||
if (blackboardModalData.type === 'object' || blackboardModalData.type === 'array') {
|
||||
try {
|
||||
if (typeof blackboardModalData.defaultValue === 'string') {
|
||||
finalValue = blackboardModalData.defaultValue ? JSON.parse(blackboardModalData.defaultValue) : (blackboardModalData.type === 'array' ? [] : {});
|
||||
}
|
||||
} catch (error) {
|
||||
alert('JSON格式错误,请检查输入');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const constraints: BlackboardVariable['constraints'] = {};
|
||||
if (blackboardModalData.constraints.min !== undefined) constraints.min = blackboardModalData.constraints.min;
|
||||
if (blackboardModalData.constraints.max !== undefined) constraints.max = blackboardModalData.constraints.max;
|
||||
if (blackboardModalData.constraints.step !== undefined) constraints.step = blackboardModalData.constraints.step;
|
||||
|
||||
if (blackboardModalData.useAllowedValues && blackboardModalData.allowedValuesText.trim()) {
|
||||
constraints.allowedValues = blackboardModalData.allowedValuesText
|
||||
.split('\n')
|
||||
.map(val => val.trim())
|
||||
.filter(val => val.length > 0);
|
||||
}
|
||||
|
||||
const variable: BlackboardVariable = {
|
||||
name: blackboardModalData.name,
|
||||
type: blackboardModalData.type,
|
||||
value: finalValue,
|
||||
defaultValue: finalValue,
|
||||
description: blackboardModalData.description,
|
||||
group: blackboardModalData.group || undefined,
|
||||
readOnly: blackboardModalData.readOnly,
|
||||
constraints: Object.keys(constraints).length > 0 ? constraints : undefined
|
||||
};
|
||||
|
||||
blackboardVariables.value.set(variable.name, variable);
|
||||
|
||||
const groupName = variable.group || '未分组';
|
||||
expandedGroups.value.add(groupName);
|
||||
|
||||
showBlackboardModal.value = false;
|
||||
editingBlackboardVariable.value = null;
|
||||
|
||||
Object.assign(blackboardModalData, {
|
||||
name: '',
|
||||
type: 'string',
|
||||
defaultValue: '',
|
||||
description: '',
|
||||
group: '',
|
||||
readOnly: false,
|
||||
constraints: {},
|
||||
useAllowedValues: false,
|
||||
allowedValuesText: ''
|
||||
});
|
||||
};
|
||||
|
||||
const deleteBlackboardVariable = (variableName: string) => {
|
||||
if (confirm(`确定要删除变量 "${variableName}" 吗?`)) {
|
||||
blackboardVariables.value.delete(variableName);
|
||||
}
|
||||
};
|
||||
|
||||
const updateBlackboardVariable = (variableName: string, newValue: any) => {
|
||||
const variable = blackboardVariables.value.get(variableName);
|
||||
if (!variable) return;
|
||||
|
||||
if (variable.readOnly) {
|
||||
alert('该变量为只读,无法修改');
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedVariable = { ...variable, value: newValue };
|
||||
blackboardVariables.value.set(variableName, updatedVariable);
|
||||
};
|
||||
|
||||
const selectVariable = (variable: BlackboardVariable) => {
|
||||
selectedVariable.value = variable;
|
||||
};
|
||||
|
||||
const clearBlackboard = () => {
|
||||
if (confirm('确定要清空所有变量吗?此操作不可恢复。')) {
|
||||
blackboardVariables.value.clear();
|
||||
selectedVariable.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const resetBlackboardToDefaults = () => {
|
||||
if (confirm('确定要重置所有变量到默认值吗?')) {
|
||||
blackboardVariables.value.forEach((variable, name) => {
|
||||
if (variable.defaultValue !== undefined) {
|
||||
variable.value = variable.defaultValue;
|
||||
blackboardVariables.value.set(name, { ...variable });
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const exportBlackboard = () => {
|
||||
const data = Array.from(blackboardVariables.value.values());
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
|
||||
try {
|
||||
navigator.clipboard.writeText(json);
|
||||
alert('Blackboard配置已复制到剪贴板');
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadBlackboardFromArray = (variables: BlackboardVariable[]) => {
|
||||
blackboardVariables.value.clear();
|
||||
variables.forEach(variable => {
|
||||
if (variable.name && variable.type) {
|
||||
blackboardVariables.value.set(variable.name, variable);
|
||||
|
||||
// 展开变量所在的组
|
||||
const groupName = variable.group || '未分组';
|
||||
expandedGroups.value.add(groupName);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importBlackboard = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
|
||||
input.onchange = (event) => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.target?.result as string);
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('格式错误:期望数组格式');
|
||||
}
|
||||
|
||||
let importCount = 0;
|
||||
data.forEach(varData => {
|
||||
if (varData.name && varData.type) {
|
||||
blackboardVariables.value.set(varData.name, varData);
|
||||
importCount++;
|
||||
}
|
||||
});
|
||||
|
||||
alert(`成功导入 ${importCount} 个变量`);
|
||||
} catch (error) {
|
||||
alert('导入失败:' + (error as Error).message);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
input.click();
|
||||
};
|
||||
|
||||
const onVariableDragStart = (event: DragEvent, variable: BlackboardVariable) => {
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dragData = {
|
||||
name: variable.name,
|
||||
type: variable.type,
|
||||
value: variable.value
|
||||
};
|
||||
|
||||
event.dataTransfer.setData('application/blackboard-variable', JSON.stringify(dragData));
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
|
||||
// 添加视觉反馈
|
||||
const dragElement = event.currentTarget as HTMLElement;
|
||||
if (dragElement) {
|
||||
dragElement.style.opacity = '0.8';
|
||||
setTimeout(() => {
|
||||
dragElement.style.opacity = '1';
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const editVariable = (variable: BlackboardVariable) => {
|
||||
editingBlackboardVariable.value = variable;
|
||||
|
||||
Object.assign(blackboardModalData, {
|
||||
name: variable.name,
|
||||
type: variable.type,
|
||||
defaultValue: (variable.type === 'object' || variable.type === 'array') ? JSON.stringify(variable.value, null, 2) : variable.value,
|
||||
description: variable.description || '',
|
||||
group: variable.group || '',
|
||||
readOnly: variable.readOnly || false,
|
||||
constraints: {
|
||||
min: variable.constraints?.min,
|
||||
max: variable.constraints?.max,
|
||||
step: variable.constraints?.step
|
||||
},
|
||||
useAllowedValues: !!(variable.constraints?.allowedValues?.length),
|
||||
allowedValuesText: variable.constraints?.allowedValues?.join('\n') || ''
|
||||
});
|
||||
|
||||
showBlackboardModal.value = true;
|
||||
};
|
||||
|
||||
const addBlackboardVariable = () => {
|
||||
editingBlackboardVariable.value = null;
|
||||
Object.assign(blackboardModalData, {
|
||||
name: '',
|
||||
type: 'string',
|
||||
defaultValue: '',
|
||||
description: '',
|
||||
group: '',
|
||||
readOnly: false,
|
||||
constraints: {},
|
||||
useAllowedValues: false,
|
||||
allowedValuesText: ''
|
||||
});
|
||||
showBlackboardModal.value = true;
|
||||
};
|
||||
|
||||
return {
|
||||
blackboardVariables: blackboardVariablesArray,
|
||||
selectedVariable,
|
||||
showBlackboardModal,
|
||||
editingBlackboardVariable,
|
||||
blackboardModalData,
|
||||
expandedGroups,
|
||||
blackboardVariableGroups,
|
||||
blackboardCollapsed,
|
||||
blackboardTransparent,
|
||||
|
||||
groupedBlackboardVariables,
|
||||
isGroupExpanded,
|
||||
toggleGroup,
|
||||
getVariableTypeIcon,
|
||||
formatBlackboardValue,
|
||||
hasVisibleConstraints,
|
||||
formatConstraints,
|
||||
getTypeDisplayName,
|
||||
getDisplayValue,
|
||||
|
||||
addBlackboardVariable,
|
||||
saveBlackboardVariable,
|
||||
deleteBlackboardVariable,
|
||||
removeBlackboardVariable: deleteBlackboardVariable,
|
||||
updateBlackboardVariable,
|
||||
editVariable,
|
||||
selectVariable,
|
||||
clearBlackboard,
|
||||
resetBlackboardToDefaults,
|
||||
loadBlackboardFromArray,
|
||||
exportBlackboard,
|
||||
importBlackboard,
|
||||
|
||||
onBlackboardDragStart: onVariableDragStart,
|
||||
editBlackboardVariable: editVariable,
|
||||
onBlackboardValueChange: (variable: BlackboardVariable) => {
|
||||
updateBlackboardVariable(variable.name, variable.value);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
import { Ref, ref } from 'vue';
|
||||
import { TreeNode, DragState } from '../types';
|
||||
|
||||
/**
|
||||
* 画布管理功能
|
||||
*/
|
||||
export function useCanvasManager(
|
||||
panX: Ref<number>,
|
||||
panY: Ref<number>,
|
||||
zoomLevel: Ref<number>,
|
||||
treeNodes: Ref<TreeNode[]>,
|
||||
selectedNodeId: Ref<string | null>,
|
||||
canvasAreaRef: Ref<HTMLElement | null>,
|
||||
updateConnections: () => void
|
||||
) {
|
||||
// 画布尺寸 - 使用默认值或从DOM获取
|
||||
const canvasWidth = ref(800);
|
||||
const canvasHeight = ref(600);
|
||||
|
||||
// 拖拽状态
|
||||
const dragState = ref<DragState>({
|
||||
isDraggingCanvas: false,
|
||||
isDraggingNode: false,
|
||||
dragNodeId: null,
|
||||
dragStartX: 0,
|
||||
dragStartY: 0,
|
||||
dragNodeStartX: 0,
|
||||
dragNodeStartY: 0,
|
||||
isConnecting: false,
|
||||
connectionStart: null,
|
||||
connectionEnd: { x: 0, y: 0 }
|
||||
});
|
||||
|
||||
// 如果有canvas引用,更新尺寸
|
||||
if (canvasAreaRef.value) {
|
||||
const rect = canvasAreaRef.value.getBoundingClientRect();
|
||||
canvasWidth.value = rect.width;
|
||||
canvasHeight.value = rect.height;
|
||||
}
|
||||
|
||||
// 画布操作功能
|
||||
const onCanvasWheel = (event: WheelEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const zoomSpeed = 0.1;
|
||||
const delta = event.deltaY > 0 ? -zoomSpeed : zoomSpeed;
|
||||
const newZoom = Math.max(0.1, Math.min(3, zoomLevel.value + delta));
|
||||
|
||||
zoomLevel.value = newZoom;
|
||||
};
|
||||
|
||||
const onCanvasMouseDown = (event: MouseEvent) => {
|
||||
// 只在空白区域开始画布拖拽
|
||||
if (event.target === event.currentTarget) {
|
||||
dragState.value.isDraggingCanvas = true;
|
||||
dragState.value.dragStartX = event.clientX;
|
||||
dragState.value.dragStartY = event.clientY;
|
||||
|
||||
document.addEventListener('mousemove', onCanvasMouseMove);
|
||||
document.addEventListener('mouseup', onCanvasMouseUp);
|
||||
}
|
||||
};
|
||||
|
||||
const onCanvasMouseMove = (event: MouseEvent) => {
|
||||
if (dragState.value.isDraggingCanvas) {
|
||||
const deltaX = event.clientX - dragState.value.dragStartX;
|
||||
const deltaY = event.clientY - dragState.value.dragStartY;
|
||||
|
||||
panX.value += deltaX;
|
||||
panY.value += deltaY;
|
||||
|
||||
dragState.value.dragStartX = event.clientX;
|
||||
dragState.value.dragStartY = event.clientY;
|
||||
}
|
||||
};
|
||||
|
||||
const onCanvasMouseUp = (event: MouseEvent) => {
|
||||
if (dragState.value.isDraggingCanvas) {
|
||||
dragState.value.isDraggingCanvas = false;
|
||||
|
||||
document.removeEventListener('mousemove', onCanvasMouseMove);
|
||||
document.removeEventListener('mouseup', onCanvasMouseUp);
|
||||
}
|
||||
};
|
||||
|
||||
// 缩放控制
|
||||
const zoomIn = () => {
|
||||
zoomLevel.value = Math.min(3, zoomLevel.value + 0.1);
|
||||
};
|
||||
|
||||
const zoomOut = () => {
|
||||
zoomLevel.value = Math.max(0.1, zoomLevel.value - 0.1);
|
||||
};
|
||||
|
||||
const resetZoom = () => {
|
||||
zoomLevel.value = 1;
|
||||
};
|
||||
|
||||
const centerView = () => {
|
||||
if (treeNodes.value.length === 0) {
|
||||
panX.value = 0;
|
||||
panY.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
|
||||
treeNodes.value.forEach(node => {
|
||||
// 尝试从DOM获取实际节点尺寸,否则使用默认值
|
||||
const nodeElement = document.querySelector(`[data-node-id="${node.id}"]`);
|
||||
let nodeWidth = 150;
|
||||
let nodeHeight = 80; // 使用基础高度
|
||||
|
||||
if (nodeElement) {
|
||||
const rect = nodeElement.getBoundingClientRect();
|
||||
nodeWidth = rect.width / zoomLevel.value;
|
||||
nodeHeight = rect.height / zoomLevel.value;
|
||||
}
|
||||
|
||||
minX = Math.min(minX, node.x);
|
||||
minY = Math.min(minY, node.y);
|
||||
maxX = Math.max(maxX, node.x + nodeWidth);
|
||||
maxY = Math.max(maxY, node.y + nodeHeight);
|
||||
});
|
||||
|
||||
const centerX = (minX + maxX) / 2;
|
||||
const centerY = (minY + maxY) / 2;
|
||||
|
||||
panX.value = canvasWidth.value / 2 - centerX * zoomLevel.value;
|
||||
panY.value = canvasHeight.value / 2 - centerY * zoomLevel.value;
|
||||
};
|
||||
|
||||
// 网格样式计算
|
||||
const gridStyle = () => {
|
||||
const gridSize = 20 * zoomLevel.value;
|
||||
return {
|
||||
backgroundSize: `${gridSize}px ${gridSize}px`,
|
||||
backgroundPosition: `${panX.value % gridSize}px ${panY.value % gridSize}px`
|
||||
};
|
||||
};
|
||||
|
||||
// 节点拖拽功能
|
||||
const startNodeDrag = (event: MouseEvent, node: any) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
dragState.value.isDraggingNode = true;
|
||||
dragState.value.dragNodeId = node.id;
|
||||
dragState.value.dragStartX = event.clientX;
|
||||
dragState.value.dragStartY = event.clientY;
|
||||
dragState.value.dragNodeStartX = node.x;
|
||||
dragState.value.dragNodeStartY = node.y;
|
||||
|
||||
const nodeElement = event.currentTarget as HTMLElement;
|
||||
nodeElement.classList.add('dragging');
|
||||
|
||||
document.addEventListener('mousemove', onNodeDrag);
|
||||
document.addEventListener('mouseup', onNodeDragEnd);
|
||||
};
|
||||
|
||||
const onNodeDrag = (event: MouseEvent) => {
|
||||
if (!dragState.value.isDraggingNode || !dragState.value.dragNodeId) return;
|
||||
|
||||
const deltaX = (event.clientX - dragState.value.dragStartX) / zoomLevel.value;
|
||||
const deltaY = (event.clientY - dragState.value.dragStartY) / zoomLevel.value;
|
||||
|
||||
const node = treeNodes.value.find(n => n.id === dragState.value.dragNodeId);
|
||||
if (node) {
|
||||
node.x = dragState.value.dragNodeStartX + deltaX;
|
||||
node.y = dragState.value.dragNodeStartY + deltaY;
|
||||
|
||||
updateConnections();
|
||||
}
|
||||
};
|
||||
|
||||
const onNodeDragEnd = (event: MouseEvent) => {
|
||||
if (dragState.value.isDraggingNode) {
|
||||
const draggingNodes = document.querySelectorAll('.tree-node.dragging');
|
||||
draggingNodes.forEach(node => node.classList.remove('dragging'));
|
||||
|
||||
dragState.value.isDraggingNode = false;
|
||||
dragState.value.dragNodeId = null;
|
||||
|
||||
updateConnections();
|
||||
|
||||
document.removeEventListener('mousemove', onNodeDrag);
|
||||
document.removeEventListener('mouseup', onNodeDragEnd);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
onCanvasWheel,
|
||||
onCanvasMouseDown,
|
||||
onCanvasMouseMove,
|
||||
onCanvasMouseUp,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
resetZoom,
|
||||
centerView,
|
||||
gridStyle,
|
||||
startNodeDrag
|
||||
};
|
||||
}
|
||||
@@ -1,490 +0,0 @@
|
||||
import { Ref } from 'vue';
|
||||
import { TreeNode } from '../types';
|
||||
import { NodeTemplate } from '../data/nodeTemplates';
|
||||
|
||||
/**
|
||||
* 代码生成管理
|
||||
*/
|
||||
export function useCodeGeneration(
|
||||
treeNodes: Ref<TreeNode[]>,
|
||||
nodeTemplates: Ref<NodeTemplate[]>,
|
||||
getNodeByIdLocal: (id: string) => TreeNode | undefined,
|
||||
rootNode: () => TreeNode | null,
|
||||
blackboardVariables?: Ref<Map<string, any>>
|
||||
) {
|
||||
|
||||
// 生成行为树配置JSON
|
||||
const generateBehaviorTreeConfig = () => {
|
||||
const root = rootNode();
|
||||
|
||||
if (!root) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const config: any = {
|
||||
version: "1.0.0",
|
||||
type: "behavior-tree",
|
||||
metadata: {
|
||||
createdAt: new Date().toISOString(),
|
||||
hasECSNodes: hasECSNodes(),
|
||||
nodeCount: treeNodes.value.length
|
||||
},
|
||||
tree: generateNodeConfig(root)
|
||||
};
|
||||
|
||||
// 包含黑板数据
|
||||
if (blackboardVariables && blackboardVariables.value.size > 0) {
|
||||
config.blackboard = Array.from(blackboardVariables.value.values());
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
// 生成可读的配置JSON字符串
|
||||
const generateConfigJSON = (): string => {
|
||||
const config = generateBehaviorTreeConfig();
|
||||
|
||||
if (!config) {
|
||||
return '// 请先添加根节点';
|
||||
}
|
||||
|
||||
return JSON.stringify(config, null, 2);
|
||||
};
|
||||
|
||||
// 生成TypeScript构建代码(用于运行时从配置创建行为树)
|
||||
const generateTypeScriptCode = (): string => {
|
||||
const config = generateBehaviorTreeConfig();
|
||||
|
||||
if (!config) {
|
||||
return '// 请先添加根节点';
|
||||
}
|
||||
|
||||
const { behaviorTreeImports, ecsImports } = getRequiredImports();
|
||||
|
||||
let importsCode = '';
|
||||
if (behaviorTreeImports.length > 0) {
|
||||
importsCode += `import { ${behaviorTreeImports.join(', ')}, BehaviorTreeBuilder } from '@esengine/ai';\n`;
|
||||
}
|
||||
if (ecsImports.length > 0) {
|
||||
importsCode += `import { ${ecsImports.join(', ')} } from '@esengine/ecs-framework';\n`;
|
||||
}
|
||||
|
||||
const contextType = hasECSNodes() ? 'Entity' : 'any';
|
||||
const configString = JSON.stringify(config, null, 4);
|
||||
|
||||
return `${importsCode}
|
||||
// 行为树配置
|
||||
const behaviorTreeConfig = ${configString};
|
||||
|
||||
// 从配置创建行为树
|
||||
export function createBehaviorTree<T extends ${contextType}>(context?: T): BehaviorTree<T> {
|
||||
return BehaviorTreeBuilder.fromConfig<T>(behaviorTreeConfig, context);
|
||||
}
|
||||
|
||||
// 直接导出配置(用于序列化保存)
|
||||
export const config = behaviorTreeConfig;`;
|
||||
};
|
||||
|
||||
const getRequiredImports = (): { behaviorTreeImports: string[], ecsImports: string[] } => {
|
||||
const behaviorTreeImports = new Set<string>();
|
||||
const ecsImports = new Set<string>();
|
||||
|
||||
// 总是需要这些基础类
|
||||
behaviorTreeImports.add('BehaviorTree');
|
||||
behaviorTreeImports.add('TaskStatus');
|
||||
|
||||
treeNodes.value.forEach(node => {
|
||||
const template = nodeTemplates.value.find(t => t.className === node.type || t.type === node.type);
|
||||
if (template?.className) {
|
||||
if (template.namespace?.includes('ecs-integration')) {
|
||||
behaviorTreeImports.add(template.className);
|
||||
ecsImports.add('Entity');
|
||||
ecsImports.add('Component');
|
||||
} else {
|
||||
behaviorTreeImports.add(template.className);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
behaviorTreeImports: Array.from(behaviorTreeImports),
|
||||
ecsImports: Array.from(ecsImports)
|
||||
};
|
||||
};
|
||||
|
||||
const hasECSNodes = (): boolean => {
|
||||
return treeNodes.value.some(node => {
|
||||
const template = nodeTemplates.value.find(t => t.className === node.type || t.type === node.type);
|
||||
return template?.namespace?.includes('ecs-integration');
|
||||
});
|
||||
};
|
||||
|
||||
// 生成节点配置对象
|
||||
const generateNodeConfig = (node: TreeNode): any => {
|
||||
const template = nodeTemplates.value.find(t => t.className === node.type || t.type === node.type);
|
||||
|
||||
if (!template || !template.className) {
|
||||
return {
|
||||
type: node.type,
|
||||
error: "未知节点类型"
|
||||
};
|
||||
}
|
||||
|
||||
const nodeConfig: any = {
|
||||
id: node.id,
|
||||
type: template.className,
|
||||
namespace: template.namespace || 'behaviourTree',
|
||||
properties: {}
|
||||
};
|
||||
|
||||
// 处理节点属性
|
||||
if (node.properties) {
|
||||
Object.entries(node.properties).forEach(([key, prop]) => {
|
||||
if (prop.value !== undefined && prop.value !== '') {
|
||||
nodeConfig.properties[key] = {
|
||||
type: prop.type,
|
||||
value: prop.value
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理子节点
|
||||
if (node.children && node.children.length > 0) {
|
||||
nodeConfig.children = node.children
|
||||
.map(childId => getNodeByIdLocal(childId))
|
||||
.filter(Boolean)
|
||||
.map(child => generateNodeConfig(child!));
|
||||
}
|
||||
|
||||
return nodeConfig;
|
||||
};
|
||||
|
||||
const generateNodeCode = (node: TreeNode, indent: number = 0): string => {
|
||||
const spaces = ' '.repeat(indent);
|
||||
const template = nodeTemplates.value.find(t => t.className === node.type || t.type === node.type);
|
||||
|
||||
if (!template || !template.className) {
|
||||
return `${spaces}// 未知节点类型: ${node.type}`;
|
||||
}
|
||||
|
||||
let code = `${spaces}new ${template.className}(`;
|
||||
const params: string[] = [];
|
||||
|
||||
// 处理特定节点的构造函数参数
|
||||
if (template.namespace?.includes('ecs-integration')) {
|
||||
// ECS节点的特殊处理
|
||||
switch (template.className) {
|
||||
case 'HasComponentCondition':
|
||||
case 'AddComponentAction':
|
||||
case 'RemoveComponentAction':
|
||||
case 'ModifyComponentAction':
|
||||
if (node.properties?.componentType?.value) {
|
||||
params.push(node.properties.componentType.value);
|
||||
}
|
||||
if (template.className === 'AddComponentAction' && node.properties?.componentFactory?.value) {
|
||||
params.push(node.properties.componentFactory.value);
|
||||
}
|
||||
if (template.className === 'ModifyComponentAction' && node.properties?.modifierCode?.value) {
|
||||
params.push(node.properties.modifierCode.value);
|
||||
}
|
||||
break;
|
||||
case 'HasTagCondition':
|
||||
if (node.properties?.tag?.value !== undefined) {
|
||||
params.push(node.properties.tag.value.toString());
|
||||
}
|
||||
break;
|
||||
case 'IsActiveCondition':
|
||||
if (node.properties?.checkHierarchy?.value !== undefined) {
|
||||
params.push(node.properties.checkHierarchy.value.toString());
|
||||
}
|
||||
break;
|
||||
case 'WaitTimeAction':
|
||||
if (node.properties?.waitTime?.value !== undefined) {
|
||||
params.push(node.properties.waitTime.value.toString());
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// 普通行为树节点的处理
|
||||
switch (template.className) {
|
||||
case 'ExecuteAction':
|
||||
case 'ExecuteActionConditional':
|
||||
if (node.properties?.actionCode?.value || node.properties?.conditionCode?.value) {
|
||||
const code = node.properties.actionCode?.value || node.properties.conditionCode?.value;
|
||||
params.push(code);
|
||||
if (node.properties?.actionName?.value) {
|
||||
params.push(`{ name: "${node.properties.actionName.value}" }`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'WaitAction':
|
||||
if (node.properties?.waitTime?.value !== undefined) {
|
||||
params.push(node.properties.waitTime.value.toString());
|
||||
}
|
||||
break;
|
||||
case 'LogAction':
|
||||
if (node.properties?.message?.value) {
|
||||
params.push(`"${node.properties.message.value}"`);
|
||||
}
|
||||
break;
|
||||
case 'Repeater':
|
||||
if (node.properties?.repeatCount?.value !== undefined) {
|
||||
params.push(node.properties.repeatCount.value.toString());
|
||||
}
|
||||
break;
|
||||
case 'Sequence':
|
||||
case 'Selector':
|
||||
if (node.properties?.abortType?.value && node.properties.abortType.value !== 'None') {
|
||||
params.push(`AbortTypes.${node.properties.abortType.value}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
code += params.join(', ');
|
||||
code += ')';
|
||||
|
||||
// 处理子节点(对于复合节点和装饰器)
|
||||
if (template.canHaveChildren && node.children && node.children.length > 0) {
|
||||
const children = node.children
|
||||
.map(childId => getNodeByIdLocal(childId))
|
||||
.filter(Boolean)
|
||||
.map(child => generateNodeCode(child!, indent + 1));
|
||||
|
||||
if (children.length > 0) {
|
||||
const className = template.className; // 保存到局部变量
|
||||
if (template.category === 'decorator') {
|
||||
// 装饰器只有一个子节点
|
||||
code = code.slice(0, -1); // 移除最后的 ')'
|
||||
const varName = className.toLowerCase();
|
||||
code += `;\n${spaces}${varName}.child = ${children[0].trim()};\n${spaces}return ${varName}`;
|
||||
} else if (template.category === 'composite') {
|
||||
// 复合节点需要添加子节点
|
||||
code = code.slice(0, -1); // 移除最后的 ')'
|
||||
code += `;\n`;
|
||||
children.forEach(child => {
|
||||
code += `${spaces}${className.toLowerCase()}.addChild(${child.trim()});\n`;
|
||||
});
|
||||
code += `${spaces}return ${className.toLowerCase()}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return code;
|
||||
};
|
||||
|
||||
// 从配置创建行为树节点
|
||||
const createTreeFromConfig = (config: any): TreeNode[] => {
|
||||
console.log('createTreeFromConfig被调用,接收到的配置:', config);
|
||||
console.log('nodeTemplates当前数量:', nodeTemplates.value.length);
|
||||
|
||||
// 处理两种不同的文件格式
|
||||
if (config.nodes && Array.isArray(config.nodes)) {
|
||||
console.log('使用nodes格式处理,节点数量:', config.nodes.length);
|
||||
const result = createTreeFromNodesFormat(config);
|
||||
console.log('nodes格式处理结果:', result);
|
||||
return result;
|
||||
} else if (config.tree) {
|
||||
console.log('使用tree格式处理');
|
||||
const result = createTreeFromTreeFormat(config);
|
||||
console.log('tree格式处理结果:', result);
|
||||
return result;
|
||||
} else {
|
||||
console.log('配置格式不匹配,返回空数组');
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 处理新格式(nodes数组格式)
|
||||
const createTreeFromNodesFormat = (config: any): TreeNode[] => {
|
||||
console.log('createTreeFromNodesFormat开始处理');
|
||||
|
||||
if (!config.nodes || !Array.isArray(config.nodes)) {
|
||||
console.log('nodes数据无效');
|
||||
return [];
|
||||
}
|
||||
|
||||
const nodes: TreeNode[] = [];
|
||||
|
||||
config.nodes.forEach((nodeConfig: any, index: number) => {
|
||||
console.log(`处理第${index + 1}个节点:`, nodeConfig);
|
||||
|
||||
const template = findTemplateByType(nodeConfig.type);
|
||||
console.log(`为节点类型 "${nodeConfig.type}" 找到的模板:`, template);
|
||||
|
||||
if (!template) {
|
||||
console.warn(`未找到节点类型 "${nodeConfig.type}" 的模板`);
|
||||
return;
|
||||
}
|
||||
|
||||
const node: TreeNode = {
|
||||
id: nodeConfig.id || generateNodeId(),
|
||||
type: template.type,
|
||||
name: nodeConfig.name || template.name,
|
||||
icon: nodeConfig.icon || template.icon,
|
||||
description: nodeConfig.description || template.description,
|
||||
canHaveChildren: template.canHaveChildren,
|
||||
canHaveParent: template.canHaveParent,
|
||||
x: nodeConfig.x || 400,
|
||||
y: nodeConfig.y || 100,
|
||||
properties: {},
|
||||
children: nodeConfig.children || [],
|
||||
parent: nodeConfig.parent,
|
||||
hasError: false
|
||||
};
|
||||
|
||||
// 恢复属性
|
||||
if (nodeConfig.properties && template.properties) {
|
||||
Object.entries(nodeConfig.properties).forEach(([key, propConfig]: [string, any]) => {
|
||||
if (template.properties![key]) {
|
||||
node.properties![key] = {
|
||||
...template.properties![key],
|
||||
value: propConfig.value !== undefined ? propConfig.value : template.properties![key].value
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 确保所有模板属性都有默认值
|
||||
if (template.properties) {
|
||||
Object.entries(template.properties).forEach(([key, propDef]) => {
|
||||
if (!node.properties![key]) {
|
||||
node.properties![key] = { ...propDef };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`创建的节点:`, node);
|
||||
nodes.push(node);
|
||||
});
|
||||
|
||||
console.log(`createTreeFromNodesFormat完成,总共创建了${nodes.length}个节点`);
|
||||
return nodes;
|
||||
};
|
||||
|
||||
// 处理旧格式(tree对象格式)
|
||||
const createTreeFromTreeFormat = (config: any): TreeNode[] => {
|
||||
if (!config.tree) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const nodes: TreeNode[] = [];
|
||||
const processNode = (nodeConfig: any, parent?: TreeNode): TreeNode => {
|
||||
const template = findTemplateByType(nodeConfig.type);
|
||||
if (!template) {
|
||||
throw new Error(`未知节点类型: ${nodeConfig.type}`);
|
||||
}
|
||||
|
||||
const node: TreeNode = {
|
||||
id: nodeConfig.id || generateNodeId(),
|
||||
type: template.type,
|
||||
name: template.name,
|
||||
icon: template.icon,
|
||||
description: template.description,
|
||||
canHaveChildren: template.canHaveChildren,
|
||||
canHaveParent: template.canHaveParent,
|
||||
x: 400,
|
||||
y: 100,
|
||||
properties: {},
|
||||
children: [],
|
||||
parent: parent?.id,
|
||||
hasError: false
|
||||
};
|
||||
|
||||
// 恢复属性
|
||||
if (nodeConfig.properties && template.properties) {
|
||||
Object.entries(nodeConfig.properties).forEach(([key, propConfig]: [string, any]) => {
|
||||
if (template.properties![key]) {
|
||||
node.properties![key] = {
|
||||
...template.properties![key],
|
||||
value: propConfig.value !== undefined ? propConfig.value : template.properties![key].value
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 确保所有模板属性都有默认值
|
||||
if (template.properties) {
|
||||
Object.entries(template.properties).forEach(([key, propDef]) => {
|
||||
if (!node.properties![key]) {
|
||||
node.properties![key] = { ...propDef };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
nodes.push(node);
|
||||
|
||||
// 处理子节点
|
||||
if (nodeConfig.children && Array.isArray(nodeConfig.children)) {
|
||||
nodeConfig.children.forEach((childConfig: any) => {
|
||||
const childNode = processNode(childConfig, node);
|
||||
node.children!.push(childNode.id);
|
||||
});
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
processNode(config.tree);
|
||||
return nodes;
|
||||
};
|
||||
|
||||
// 通过类型名查找模板(支持多种匹配方式)
|
||||
const findTemplateByType = (typeName: string): NodeTemplate | undefined => {
|
||||
// 直接匹配 type 字段
|
||||
let template = nodeTemplates.value.find(t => t.type === typeName);
|
||||
if (template) return template;
|
||||
|
||||
// 匹配 className 字段
|
||||
template = nodeTemplates.value.find(t => t.className === typeName);
|
||||
if (template) return template;
|
||||
|
||||
// 大小写不敏感匹配 type
|
||||
template = nodeTemplates.value.find(t => t.type.toLowerCase() === typeName.toLowerCase());
|
||||
if (template) return template;
|
||||
|
||||
// 大小写不敏感匹配 className
|
||||
template = nodeTemplates.value.find(t => t.className && t.className.toLowerCase() === typeName.toLowerCase());
|
||||
if (template) return template;
|
||||
|
||||
// 特殊映射处理
|
||||
const typeMapping: Record<string, string> = {
|
||||
'Sequence': 'sequence',
|
||||
'Selector': 'selector',
|
||||
'Parallel': 'parallel',
|
||||
'Inverter': 'inverter',
|
||||
'Repeater': 'repeater',
|
||||
'AlwaysSucceed': 'always-succeed',
|
||||
'AlwaysFail': 'always-fail',
|
||||
'UntilSuccess': 'until-success',
|
||||
'UntilFail': 'until-fail',
|
||||
'ExecuteAction': 'execute-action',
|
||||
'LogAction': 'log-action',
|
||||
'WaitAction': 'wait-action'
|
||||
};
|
||||
|
||||
const mappedType = typeMapping[typeName];
|
||||
if (mappedType) {
|
||||
template = nodeTemplates.value.find(t => t.type === mappedType);
|
||||
if (template) return template;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// 生成唯一节点ID
|
||||
const generateNodeId = (): string => {
|
||||
return 'node_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
};
|
||||
|
||||
return {
|
||||
generateBehaviorTreeConfig,
|
||||
generateConfigJSON,
|
||||
generateTypeScriptCode,
|
||||
generateNodeCode,
|
||||
generateNodeConfig,
|
||||
createTreeFromConfig,
|
||||
getRequiredImports
|
||||
};
|
||||
}
|
||||
@@ -1,372 +0,0 @@
|
||||
import { Ref, computed } from 'vue';
|
||||
import { TreeNode } from '../types';
|
||||
import { NodeTemplate } from '../data/nodeTemplates';
|
||||
import { getRootNode } from '../utils/nodeUtils';
|
||||
import { getInstallStatusText, getInstallStatusClass } from '../utils/installUtils';
|
||||
import { getGridStyle } from '../utils/canvasUtils';
|
||||
|
||||
/**
|
||||
* 计算属性管理
|
||||
*/
|
||||
export function useComputedProperties(
|
||||
nodeTemplates: Ref<NodeTemplate[]>,
|
||||
nodeSearchText: Ref<string>,
|
||||
treeNodes: Ref<TreeNode[]>,
|
||||
selectedNodeId: Ref<string | null>,
|
||||
selectedConditionNodeId: Ref<string | null>,
|
||||
checkingStatus: Ref<boolean>,
|
||||
isInstalling: Ref<boolean>,
|
||||
isInstalled: Ref<boolean>,
|
||||
version: Ref<string | null>,
|
||||
exportFormat: Ref<string>,
|
||||
panX: Ref<number>,
|
||||
panY: Ref<number>,
|
||||
zoomLevel: Ref<number>,
|
||||
getNodeByIdLocal: (id: string) => TreeNode | undefined,
|
||||
codeGeneration?: {
|
||||
generateConfigJSON: () => string;
|
||||
generateTypeScriptCode: () => string;
|
||||
}
|
||||
) {
|
||||
// 过滤节点
|
||||
const filteredRootNodes = () => {
|
||||
return nodeTemplates.value.filter(node =>
|
||||
node.category === 'root' &&
|
||||
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
|
||||
);
|
||||
};
|
||||
|
||||
const filteredCompositeNodes = () => {
|
||||
return nodeTemplates.value.filter(node =>
|
||||
node.category === 'composite' &&
|
||||
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
|
||||
);
|
||||
};
|
||||
|
||||
const filteredDecoratorNodes = () => {
|
||||
return nodeTemplates.value.filter(node =>
|
||||
node.category === 'decorator' &&
|
||||
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
|
||||
);
|
||||
};
|
||||
|
||||
const filteredActionNodes = () => {
|
||||
return nodeTemplates.value.filter(node =>
|
||||
node.category === 'action' &&
|
||||
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
|
||||
);
|
||||
};
|
||||
|
||||
const filteredConditionNodes = () => {
|
||||
return nodeTemplates.value.filter(node =>
|
||||
node.category === 'condition' &&
|
||||
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
|
||||
);
|
||||
};
|
||||
|
||||
const filteredECSNodes = () => {
|
||||
return nodeTemplates.value.filter(node =>
|
||||
node.category === 'ecs' &&
|
||||
node.name.toLowerCase().includes(nodeSearchText.value.toLowerCase())
|
||||
);
|
||||
};
|
||||
|
||||
// 选中的节点 - 使用computed确保响应式更新
|
||||
const selectedNode = computed(() => {
|
||||
if (!selectedNodeId.value) return null;
|
||||
|
||||
// 直接从treeNodes数组中查找,确保获取最新的节点状态
|
||||
const node = treeNodes.value.find(n => n.id === selectedNodeId.value);
|
||||
return node || null;
|
||||
});
|
||||
|
||||
// 当前选中的条件节点(用于编辑条件属性)
|
||||
const selectedConditionNode = computed(() => {
|
||||
if (!selectedConditionNodeId.value) return null;
|
||||
const decoratorNode = treeNodes.value.find(n => n.id === selectedConditionNodeId.value);
|
||||
if (!decoratorNode || !decoratorNode.attachedCondition) return null;
|
||||
|
||||
// 根据条件类型重新构建属性结构
|
||||
const conditionProperties = reconstructConditionProperties(
|
||||
decoratorNode.attachedCondition.type,
|
||||
decoratorNode.properties || {}
|
||||
);
|
||||
|
||||
// 创建一个虚拟的条件节点对象,用于属性编辑
|
||||
return {
|
||||
id: decoratorNode.id + '_condition',
|
||||
name: decoratorNode.attachedCondition.name + '(条件)',
|
||||
type: decoratorNode.attachedCondition.type,
|
||||
icon: decoratorNode.attachedCondition.icon,
|
||||
properties: conditionProperties,
|
||||
isConditionNode: true,
|
||||
parentDecorator: decoratorNode
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 根据条件类型重新构建属性结构
|
||||
* 将装饰器的扁平属性转换回条件模板的属性结构
|
||||
*/
|
||||
const reconstructConditionProperties = (conditionType: string, decoratorProperties: Record<string, any>) => {
|
||||
switch (conditionType) {
|
||||
case 'condition-random':
|
||||
return {
|
||||
successProbability: {
|
||||
type: 'number',
|
||||
name: '成功概率',
|
||||
value: decoratorProperties.successProbability || 0.5,
|
||||
description: '条件成功的概率 (0.0 - 1.0)'
|
||||
}
|
||||
};
|
||||
|
||||
case 'condition-component':
|
||||
return {
|
||||
componentType: {
|
||||
type: 'string',
|
||||
name: '组件类型',
|
||||
value: decoratorProperties.componentType || '',
|
||||
description: '要检查的组件类型名称'
|
||||
}
|
||||
};
|
||||
|
||||
case 'condition-tag':
|
||||
return {
|
||||
tagValue: {
|
||||
type: 'number',
|
||||
name: '标签值',
|
||||
value: decoratorProperties.tagValue || 0,
|
||||
description: '要检查的标签值'
|
||||
}
|
||||
};
|
||||
|
||||
case 'condition-active':
|
||||
return {
|
||||
checkHierarchy: {
|
||||
type: 'boolean',
|
||||
name: '检查层级激活',
|
||||
value: decoratorProperties.checkHierarchy || false,
|
||||
description: '是否检查整个层级的激活状态'
|
||||
}
|
||||
};
|
||||
|
||||
case 'condition-numeric':
|
||||
return {
|
||||
propertyPath: {
|
||||
type: 'string',
|
||||
name: '属性路径',
|
||||
value: decoratorProperties.propertyPath || 'context.someValue',
|
||||
description: '要比较的数值属性路径'
|
||||
},
|
||||
compareOperator: {
|
||||
type: 'select',
|
||||
name: '比较操作符',
|
||||
value: decoratorProperties.compareOperator || 'greater',
|
||||
options: ['greater', 'less', 'equal', 'greaterEqual', 'lessEqual', 'notEqual'],
|
||||
description: '数值比较的操作符'
|
||||
},
|
||||
compareValue: {
|
||||
type: 'number',
|
||||
name: '比较值',
|
||||
value: decoratorProperties.compareValue || 0,
|
||||
description: '用于比较的目标值'
|
||||
}
|
||||
};
|
||||
|
||||
case 'condition-property':
|
||||
return {
|
||||
propertyPath: {
|
||||
type: 'string',
|
||||
name: '属性路径',
|
||||
value: decoratorProperties.propertyPath || 'context.someProperty',
|
||||
description: '要检查的属性路径'
|
||||
}
|
||||
};
|
||||
|
||||
case 'condition-custom':
|
||||
return {
|
||||
conditionCode: {
|
||||
type: 'code',
|
||||
name: '条件代码',
|
||||
value: decoratorProperties.conditionCode || '(context) => true',
|
||||
description: '自定义条件判断函数'
|
||||
}
|
||||
};
|
||||
|
||||
// Blackboard相关条件(使用实际的模板类型名)
|
||||
case 'blackboard-variable-exists':
|
||||
return {
|
||||
variableName: {
|
||||
type: 'string',
|
||||
name: '变量名',
|
||||
value: decoratorProperties.variableName || '',
|
||||
description: '要检查的黑板变量名'
|
||||
},
|
||||
invert: {
|
||||
type: 'boolean',
|
||||
name: '反转结果',
|
||||
value: decoratorProperties.invert || false,
|
||||
description: '是否反转检查结果'
|
||||
}
|
||||
};
|
||||
|
||||
case 'blackboard-value-comparison':
|
||||
return {
|
||||
variableName: {
|
||||
type: 'string',
|
||||
name: '变量名',
|
||||
value: decoratorProperties.variableName || '',
|
||||
description: '要比较的黑板变量名'
|
||||
},
|
||||
operator: {
|
||||
type: 'select',
|
||||
name: '比较操作符',
|
||||
value: decoratorProperties.operator || 'equal',
|
||||
options: ['equal', 'notEqual', 'greater', 'greaterOrEqual', 'less', 'lessOrEqual', 'contains', 'notContains'],
|
||||
description: '比较操作类型'
|
||||
},
|
||||
compareValue: {
|
||||
type: 'string',
|
||||
name: '比较值',
|
||||
value: decoratorProperties.compareValue || '',
|
||||
description: '用于比较的值(留空则使用比较变量)'
|
||||
},
|
||||
compareVariable: {
|
||||
type: 'string',
|
||||
name: '比较变量名',
|
||||
value: decoratorProperties.compareVariable || '',
|
||||
description: '用于比较的另一个黑板变量名'
|
||||
}
|
||||
};
|
||||
|
||||
case 'blackboard-variable-type-check':
|
||||
return {
|
||||
variableName: {
|
||||
type: 'string',
|
||||
name: '变量名',
|
||||
value: decoratorProperties.variableName || '',
|
||||
description: '要检查的黑板变量名'
|
||||
},
|
||||
expectedType: {
|
||||
type: 'select',
|
||||
name: '期望类型',
|
||||
value: decoratorProperties.expectedType || 'string',
|
||||
options: ['string', 'number', 'boolean', 'vector2', 'vector3', 'object', 'array'],
|
||||
description: '期望的变量类型'
|
||||
}
|
||||
};
|
||||
|
||||
case 'blackboard-variable-range-check':
|
||||
return {
|
||||
variableName: {
|
||||
type: 'string',
|
||||
name: '变量名',
|
||||
value: decoratorProperties.variableName || '',
|
||||
description: '要检查的数值型黑板变量名'
|
||||
},
|
||||
minValue: {
|
||||
type: 'number',
|
||||
name: '最小值',
|
||||
value: decoratorProperties.minValue || 0,
|
||||
description: '范围的最小值(包含)'
|
||||
},
|
||||
maxValue: {
|
||||
type: 'number',
|
||||
name: '最大值',
|
||||
value: decoratorProperties.maxValue || 100,
|
||||
description: '范围的最大值(包含)'
|
||||
}
|
||||
};
|
||||
|
||||
default:
|
||||
// 对于未知的条件类型,尝试从装饰器属性中推断
|
||||
const reconstructed: Record<string, any> = {};
|
||||
Object.keys(decoratorProperties).forEach(key => {
|
||||
if (key !== 'conditionType') {
|
||||
reconstructed[key] = {
|
||||
type: typeof decoratorProperties[key] === 'number' ? 'number' :
|
||||
typeof decoratorProperties[key] === 'boolean' ? 'boolean' : 'string',
|
||||
name: key,
|
||||
value: decoratorProperties[key],
|
||||
description: `${key}参数`
|
||||
};
|
||||
}
|
||||
});
|
||||
return reconstructed;
|
||||
}
|
||||
};
|
||||
|
||||
// 当前显示在属性面板的节点(普通节点或条件节点)
|
||||
const activeNode = computed(() => selectedConditionNode.value || selectedNode.value);
|
||||
|
||||
// 根节点
|
||||
const rootNode = () => {
|
||||
return getRootNode(treeNodes.value);
|
||||
};
|
||||
|
||||
// 安装状态
|
||||
const installStatusClass = () => {
|
||||
return getInstallStatusClass(isInstalling.value, isInstalled.value);
|
||||
};
|
||||
|
||||
const installStatusText = () => {
|
||||
return getInstallStatusText(
|
||||
checkingStatus.value,
|
||||
isInstalling.value,
|
||||
isInstalled.value,
|
||||
version.value
|
||||
);
|
||||
};
|
||||
|
||||
// 验证结果
|
||||
const validationResult = () => {
|
||||
if (treeNodes.value.length === 0) {
|
||||
return { isValid: false, message: '行为树为空' };
|
||||
}
|
||||
const root = rootNode();
|
||||
if (!root) {
|
||||
return { isValid: false, message: '缺少根节点' };
|
||||
}
|
||||
return { isValid: true, message: '行为树结构有效' };
|
||||
};
|
||||
|
||||
// 导出代码
|
||||
const exportedCode = () => {
|
||||
if (!codeGeneration) {
|
||||
return '// 代码生成器未初始化';
|
||||
}
|
||||
|
||||
try {
|
||||
if (exportFormat.value === 'json') {
|
||||
return codeGeneration.generateConfigJSON();
|
||||
} else {
|
||||
return codeGeneration.generateTypeScriptCode();
|
||||
}
|
||||
} catch (error) {
|
||||
return `// 代码生成失败: ${error}`;
|
||||
}
|
||||
};
|
||||
|
||||
// 网格样式
|
||||
const gridStyle = () => {
|
||||
return getGridStyle(panX.value, panY.value, zoomLevel.value);
|
||||
};
|
||||
|
||||
return {
|
||||
filteredRootNodes,
|
||||
filteredCompositeNodes,
|
||||
filteredDecoratorNodes,
|
||||
filteredActionNodes,
|
||||
filteredConditionNodes,
|
||||
filteredECSNodes,
|
||||
selectedNode,
|
||||
selectedConditionNode,
|
||||
activeNode,
|
||||
rootNode,
|
||||
installStatusClass,
|
||||
installStatusText,
|
||||
validationResult,
|
||||
exportedCode,
|
||||
gridStyle
|
||||
};
|
||||
}
|
||||
@@ -1,482 +0,0 @@
|
||||
import { ref, reactive, Ref } from 'vue';
|
||||
import { TreeNode } from '../types';
|
||||
import { NodeTemplate } from '../data/nodeTemplates';
|
||||
|
||||
/**
|
||||
* 拖拽状态
|
||||
*/
|
||||
interface DragState {
|
||||
isDraggingCondition: boolean;
|
||||
conditionTemplate: NodeTemplate | null;
|
||||
mousePosition: { x: number, y: number } | null;
|
||||
hoveredDecoratorId: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 条件节点吸附功能
|
||||
*/
|
||||
export function useConditionAttachment(
|
||||
treeNodes: Ref<TreeNode[]>,
|
||||
getNodeByIdLocal: (id: string) => TreeNode | undefined
|
||||
) {
|
||||
|
||||
const dragState = reactive<DragState>({
|
||||
isDraggingCondition: false,
|
||||
conditionTemplate: null,
|
||||
mousePosition: null,
|
||||
hoveredDecoratorId: null
|
||||
});
|
||||
|
||||
/**
|
||||
* 检查节点是否为条件装饰器
|
||||
*/
|
||||
const isConditionalDecorator = (node: TreeNode): boolean => {
|
||||
return node.type === 'conditional-decorator';
|
||||
};
|
||||
|
||||
/**
|
||||
* 开始拖拽条件节点
|
||||
*/
|
||||
const startConditionDrag = (event: DragEvent, template: NodeTemplate) => {
|
||||
console.log('🎯 开始条件拖拽:', template.name, template.isDraggableCondition);
|
||||
|
||||
if (!template.isDraggableCondition) {
|
||||
console.warn('节点不是可拖拽条件:', template.name);
|
||||
return;
|
||||
}
|
||||
|
||||
dragState.isDraggingCondition = true;
|
||||
dragState.conditionTemplate = template;
|
||||
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('application/json', JSON.stringify({
|
||||
...template,
|
||||
isConditionDrag: true
|
||||
}));
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
}
|
||||
|
||||
console.log('✅ 条件拖拽状态已设置:', dragState);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理拖拽悬停在装饰器上
|
||||
*/
|
||||
const handleDecoratorDragOver = (event: DragEvent, decoratorNode: TreeNode) => {
|
||||
console.log('🔀 装饰器拖拽悬停:', decoratorNode.name, decoratorNode.type, 'isDragging:', dragState.isDraggingCondition);
|
||||
|
||||
// 检查传输数据
|
||||
const transferData = event.dataTransfer?.getData('application/json');
|
||||
if (transferData) {
|
||||
try {
|
||||
const data = JSON.parse(transferData);
|
||||
console.log('📦 传输数据:', data.isConditionDrag, data.isDraggableCondition, data.name);
|
||||
} catch (e) {
|
||||
console.log('❌ 传输数据解析失败:', transferData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!dragState.isDraggingCondition || !isConditionalDecorator(decoratorNode)) {
|
||||
console.log('❌ 不符合条件:', {
|
||||
isDragging: dragState.isDraggingCondition,
|
||||
isDecorator: isConditionalDecorator(decoratorNode),
|
||||
nodeType: decoratorNode.type
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
dragState.hoveredDecoratorId = decoratorNode.id;
|
||||
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
|
||||
console.log('✅ 装饰器可接受拖拽:', decoratorNode.name);
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理拖拽离开装饰器
|
||||
*/
|
||||
const handleDecoratorDragLeave = (decoratorNode: TreeNode) => {
|
||||
if (dragState.hoveredDecoratorId === decoratorNode.id) {
|
||||
dragState.hoveredDecoratorId = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 条件到装饰器属性的映射
|
||||
*/
|
||||
const mapConditionToDecoratorProperties = (conditionTemplate: NodeTemplate): Record<string, any> => {
|
||||
const baseConfig = {
|
||||
conditionType: getConditionTypeFromTemplate(conditionTemplate),
|
||||
shouldReevaluate: true
|
||||
};
|
||||
|
||||
switch (conditionTemplate.type) {
|
||||
case 'condition-random':
|
||||
return {
|
||||
...baseConfig,
|
||||
successProbability: conditionTemplate.properties?.successProbability?.value || 0.5
|
||||
};
|
||||
|
||||
case 'condition-component':
|
||||
return {
|
||||
...baseConfig,
|
||||
componentType: conditionTemplate.properties?.componentType?.value || 'Component'
|
||||
};
|
||||
|
||||
case 'condition-tag':
|
||||
return {
|
||||
...baseConfig,
|
||||
tagValue: conditionTemplate.properties?.tagValue?.value || 0
|
||||
};
|
||||
|
||||
case 'condition-active':
|
||||
return {
|
||||
...baseConfig,
|
||||
checkHierarchy: conditionTemplate.properties?.checkHierarchy?.value || true
|
||||
};
|
||||
|
||||
case 'condition-numeric':
|
||||
return {
|
||||
...baseConfig,
|
||||
propertyPath: conditionTemplate.properties?.propertyPath?.value || 'context.someValue',
|
||||
compareOperator: conditionTemplate.properties?.compareOperator?.value || 'greater',
|
||||
compareValue: conditionTemplate.properties?.compareValue?.value || 0
|
||||
};
|
||||
|
||||
case 'condition-property':
|
||||
return {
|
||||
...baseConfig,
|
||||
propertyPath: conditionTemplate.properties?.propertyPath?.value || 'context.someProperty'
|
||||
};
|
||||
|
||||
case 'condition-custom':
|
||||
return {
|
||||
...baseConfig,
|
||||
conditionCode: conditionTemplate.properties?.conditionCode?.value || '(context) => true'
|
||||
};
|
||||
|
||||
// Blackboard相关条件支持
|
||||
case 'blackboard-variable-exists':
|
||||
return {
|
||||
...baseConfig,
|
||||
variableName: conditionTemplate.properties?.variableName?.value || '',
|
||||
invert: conditionTemplate.properties?.invert?.value || false
|
||||
};
|
||||
|
||||
case 'blackboard-value-comparison':
|
||||
return {
|
||||
...baseConfig,
|
||||
variableName: conditionTemplate.properties?.variableName?.value || '',
|
||||
operator: conditionTemplate.properties?.operator?.value || 'equal',
|
||||
compareValue: conditionTemplate.properties?.compareValue?.value || '',
|
||||
compareVariable: conditionTemplate.properties?.compareVariable?.value || ''
|
||||
};
|
||||
|
||||
case 'blackboard-variable-type-check':
|
||||
return {
|
||||
...baseConfig,
|
||||
variableName: conditionTemplate.properties?.variableName?.value || '',
|
||||
expectedType: conditionTemplate.properties?.expectedType?.value || 'string'
|
||||
};
|
||||
|
||||
case 'blackboard-variable-range-check':
|
||||
return {
|
||||
...baseConfig,
|
||||
variableName: conditionTemplate.properties?.variableName?.value || '',
|
||||
minValue: conditionTemplate.properties?.minValue?.value || 0,
|
||||
maxValue: conditionTemplate.properties?.maxValue?.value || 100
|
||||
};
|
||||
|
||||
default:
|
||||
return baseConfig;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取条件类型字符串
|
||||
*/
|
||||
const getConditionTypeFromTemplate = (template: NodeTemplate): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'condition-random': 'random',
|
||||
'condition-component': 'hasComponent',
|
||||
'condition-tag': 'hasTag',
|
||||
'condition-active': 'isActive',
|
||||
'condition-numeric': 'numericCompare',
|
||||
'condition-property': 'propertyExists',
|
||||
'condition-custom': 'custom',
|
||||
// Blackboard相关条件
|
||||
'blackboard-variable-exists': 'blackboardExists',
|
||||
'blackboard-value-comparison': 'blackboardCompare',
|
||||
'blackboard-variable-type-check': 'blackboardTypeCheck',
|
||||
'blackboard-variable-range-check': 'blackboardRangeCheck'
|
||||
};
|
||||
|
||||
return typeMap[template.type] || 'custom';
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行条件吸附到装饰器
|
||||
*/
|
||||
const attachConditionToDecorator = (
|
||||
event: DragEvent,
|
||||
decoratorNode: TreeNode
|
||||
): boolean => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (!dragState.isDraggingCondition || !dragState.conditionTemplate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isConditionalDecorator(decoratorNode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取条件配置
|
||||
const conditionConfig = mapConditionToDecoratorProperties(dragState.conditionTemplate);
|
||||
|
||||
// 更新装饰器属性
|
||||
if (!decoratorNode.properties) {
|
||||
decoratorNode.properties = {};
|
||||
}
|
||||
|
||||
Object.assign(decoratorNode.properties, conditionConfig);
|
||||
|
||||
// 标记装饰器已附加条件
|
||||
decoratorNode.attachedCondition = {
|
||||
type: dragState.conditionTemplate.type,
|
||||
name: dragState.conditionTemplate.name,
|
||||
icon: dragState.conditionTemplate.icon
|
||||
};
|
||||
|
||||
// 初始化为收缩状态
|
||||
if (decoratorNode.conditionExpanded === undefined) {
|
||||
decoratorNode.conditionExpanded = false;
|
||||
}
|
||||
|
||||
// 重置拖拽状态
|
||||
resetDragState();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理画布拖拽事件(阻止条件节点创建为独立节点)
|
||||
*/
|
||||
const handleCanvasDrop = (event: DragEvent): boolean => {
|
||||
const templateData = event.dataTransfer?.getData('application/json');
|
||||
if (!templateData) return false;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(templateData);
|
||||
// 如果是条件拖拽,阻止创建独立节点
|
||||
if (data.isConditionDrag || data.isDraggableCondition) {
|
||||
event.preventDefault();
|
||||
resetDragState();
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略解析错误
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置拖拽状态
|
||||
*/
|
||||
const resetDragState = () => {
|
||||
dragState.isDraggingCondition = false;
|
||||
dragState.conditionTemplate = null;
|
||||
dragState.mousePosition = null;
|
||||
dragState.hoveredDecoratorId = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取条件显示文本(简化版始终显示条件名称)
|
||||
*/
|
||||
const getConditionDisplayText = (decoratorNode: TreeNode, expanded: boolean = false): string => {
|
||||
if (!decoratorNode.attachedCondition) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 始终返回条件名称,不管是否展开
|
||||
return decoratorNode.attachedCondition.name;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取条件的可见属性(用于展开时显示)
|
||||
*/
|
||||
const getConditionProperties = (decoratorNode: TreeNode): Record<string, any> => {
|
||||
if (!decoratorNode.attachedCondition || !decoratorNode.properties) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const conditionType = decoratorNode.attachedCondition.type;
|
||||
const visibleProps: Record<string, any> = {};
|
||||
|
||||
// 根据条件类型筛选相关属性
|
||||
switch (conditionType) {
|
||||
case 'condition-random':
|
||||
if ('successProbability' in decoratorNode.properties) {
|
||||
visibleProps['成功概率'] = `${(decoratorNode.properties.successProbability * 100).toFixed(1)}%`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'condition-component':
|
||||
if ('componentType' in decoratorNode.properties) {
|
||||
visibleProps['组件类型'] = decoratorNode.properties.componentType;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'condition-tag':
|
||||
if ('tagValue' in decoratorNode.properties) {
|
||||
visibleProps['标签值'] = decoratorNode.properties.tagValue;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'condition-active':
|
||||
if ('checkHierarchy' in decoratorNode.properties) {
|
||||
visibleProps['检查层级'] = decoratorNode.properties.checkHierarchy ? '是' : '否';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'condition-numeric':
|
||||
if ('propertyPath' in decoratorNode.properties) {
|
||||
visibleProps['属性路径'] = decoratorNode.properties.propertyPath;
|
||||
}
|
||||
if ('compareOperator' in decoratorNode.properties) {
|
||||
visibleProps['比较操作'] = decoratorNode.properties.compareOperator;
|
||||
}
|
||||
if ('compareValue' in decoratorNode.properties) {
|
||||
visibleProps['比较值'] = decoratorNode.properties.compareValue;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'condition-property':
|
||||
if ('propertyPath' in decoratorNode.properties) {
|
||||
visibleProps['属性路径'] = decoratorNode.properties.propertyPath;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'blackboard-variable-exists':
|
||||
if ('variableName' in decoratorNode.properties) {
|
||||
visibleProps['变量名'] = decoratorNode.properties.variableName;
|
||||
}
|
||||
if ('invert' in decoratorNode.properties) {
|
||||
visibleProps['反转结果'] = decoratorNode.properties.invert ? '是' : '否';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'blackboard-value-comparison':
|
||||
if ('variableName' in decoratorNode.properties) {
|
||||
visibleProps['变量名'] = decoratorNode.properties.variableName;
|
||||
}
|
||||
if ('operator' in decoratorNode.properties) {
|
||||
visibleProps['操作符'] = decoratorNode.properties.operator;
|
||||
}
|
||||
if ('compareValue' in decoratorNode.properties) {
|
||||
visibleProps['比较值'] = decoratorNode.properties.compareValue;
|
||||
}
|
||||
if ('compareVariable' in decoratorNode.properties) {
|
||||
visibleProps['比较变量'] = decoratorNode.properties.compareVariable;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'blackboard-variable-type-check':
|
||||
if ('variableName' in decoratorNode.properties) {
|
||||
visibleProps['变量名'] = decoratorNode.properties.variableName;
|
||||
}
|
||||
if ('expectedType' in decoratorNode.properties) {
|
||||
visibleProps['期望类型'] = decoratorNode.properties.expectedType;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'blackboard-variable-range-check':
|
||||
if ('variableName' in decoratorNode.properties) {
|
||||
visibleProps['变量名'] = decoratorNode.properties.variableName;
|
||||
}
|
||||
if ('minValue' in decoratorNode.properties) {
|
||||
visibleProps['最小值'] = decoratorNode.properties.minValue;
|
||||
}
|
||||
if ('maxValue' in decoratorNode.properties) {
|
||||
visibleProps['最大值'] = decoratorNode.properties.maxValue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return visibleProps;
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换条件展开状态
|
||||
*/
|
||||
const toggleConditionExpanded = (decoratorNode: TreeNode) => {
|
||||
decoratorNode.conditionExpanded = !decoratorNode.conditionExpanded;
|
||||
};
|
||||
|
||||
/**
|
||||
* 移除装饰器的条件
|
||||
*/
|
||||
const removeConditionFromDecorator = (decoratorNode: TreeNode) => {
|
||||
if (decoratorNode.attachedCondition) {
|
||||
// 删除附加的条件信息
|
||||
delete decoratorNode.attachedCondition;
|
||||
|
||||
// 重置展开状态
|
||||
decoratorNode.conditionExpanded = false;
|
||||
|
||||
// 保留装饰器的基础属性,只删除条件相关的属性
|
||||
const preservedProperties: Record<string, any> = {};
|
||||
|
||||
// 条件装饰器的基础属性
|
||||
const baseDecoratorProperties = [
|
||||
'executeWhenTrue',
|
||||
'executeWhenFalse',
|
||||
'checkInterval',
|
||||
'abortType'
|
||||
];
|
||||
|
||||
// 保留基础属性
|
||||
if (decoratorNode.properties) {
|
||||
baseDecoratorProperties.forEach(key => {
|
||||
if (key in decoratorNode.properties!) {
|
||||
preservedProperties[key] = decoratorNode.properties![key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 重置为只包含基础属性的对象
|
||||
decoratorNode.properties = preservedProperties;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查装饰器是否可以接受条件吸附
|
||||
*/
|
||||
const canAcceptCondition = (decoratorNode: TreeNode): boolean => {
|
||||
return isConditionalDecorator(decoratorNode);
|
||||
};
|
||||
|
||||
return {
|
||||
dragState,
|
||||
startConditionDrag,
|
||||
handleDecoratorDragOver,
|
||||
handleDecoratorDragLeave,
|
||||
attachConditionToDecorator,
|
||||
handleCanvasDrop,
|
||||
resetDragState,
|
||||
getConditionDisplayText,
|
||||
removeConditionFromDecorator,
|
||||
canAcceptCondition,
|
||||
isConditionalDecorator,
|
||||
toggleConditionExpanded,
|
||||
getConditionProperties
|
||||
};
|
||||
}
|
||||
@@ -1,610 +0,0 @@
|
||||
import { Ref } from 'vue';
|
||||
import { TreeNode, Connection, ConnectionState } from '../types';
|
||||
|
||||
/**
|
||||
* 连接线管理功能
|
||||
*/
|
||||
export function useConnectionManager(
|
||||
treeNodes: Ref<TreeNode[]>,
|
||||
connections: Ref<Connection[]>,
|
||||
connectionState: ConnectionState,
|
||||
canvasAreaRef: Ref<HTMLElement | null>,
|
||||
svgRef: Ref<SVGElement | null>,
|
||||
panX: Ref<number>,
|
||||
panY: Ref<number>,
|
||||
zoomLevel: Ref<number>
|
||||
) {
|
||||
|
||||
const getPortPosition = (nodeId: string, portType: 'input' | 'output') => {
|
||||
const node = treeNodes.value.find(n => n.id === nodeId);
|
||||
if (!node) return null;
|
||||
|
||||
const canvasArea = canvasAreaRef.value;
|
||||
if (!canvasArea) {
|
||||
return getCalculatedPortPosition(node, portType);
|
||||
}
|
||||
|
||||
const selectors = [
|
||||
`[data-node-id="${nodeId}"]`,
|
||||
`.tree-node[data-node-id="${nodeId}"]`,
|
||||
`div[data-node-id="${nodeId}"]`
|
||||
];
|
||||
|
||||
let nodeElement: HTMLElement | null = null;
|
||||
|
||||
for (const selector of selectors) {
|
||||
try {
|
||||
const doc = canvasArea.ownerDocument || document;
|
||||
const foundElement = doc.querySelector(selector);
|
||||
if (foundElement && canvasArea.contains(foundElement)) {
|
||||
nodeElement = foundElement as HTMLElement;
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!nodeElement) {
|
||||
try {
|
||||
const allTreeNodes = canvasArea.querySelectorAll('.tree-node');
|
||||
for (let i = 0; i < allTreeNodes.length; i++) {
|
||||
const el = allTreeNodes[i] as HTMLElement;
|
||||
const dataNodeId = el.getAttribute('data-node-id');
|
||||
if (dataNodeId === nodeId) {
|
||||
nodeElement = el;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Fallback to calculated position
|
||||
}
|
||||
}
|
||||
|
||||
if (!nodeElement) {
|
||||
return getCalculatedPortPosition(node, portType);
|
||||
}
|
||||
|
||||
const portSelectors = [
|
||||
`.port.port-${portType}`,
|
||||
`.port-${portType}`,
|
||||
`.port.${portType}`,
|
||||
`.${portType}-port`
|
||||
];
|
||||
|
||||
let portElement: HTMLElement | null = null;
|
||||
|
||||
for (const portSelector of portSelectors) {
|
||||
try {
|
||||
portElement = nodeElement.querySelector(portSelector) as HTMLElement;
|
||||
if (portElement) {
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!portElement) {
|
||||
return getNodeEdgePortPosition(nodeElement, node, portType);
|
||||
}
|
||||
|
||||
const portRect = portElement.getBoundingClientRect();
|
||||
const canvasRect = canvasAreaRef.value?.getBoundingClientRect();
|
||||
|
||||
if (!canvasRect) {
|
||||
return getCalculatedPortPosition(node, portType);
|
||||
}
|
||||
|
||||
const relativeX = portRect.left + portRect.width / 2 - canvasRect.left;
|
||||
const relativeY = portRect.top + portRect.height / 2 - canvasRect.top;
|
||||
|
||||
const svgX = (relativeX - panX.value) / zoomLevel.value;
|
||||
const svgY = (relativeY - panY.value) / zoomLevel.value;
|
||||
|
||||
return { x: svgX, y: svgY };
|
||||
};
|
||||
|
||||
const getCalculatedPortPosition = (node: any, portType: 'input' | 'output') => {
|
||||
let nodeWidth = 150;
|
||||
let nodeHeight = 80;
|
||||
|
||||
if (node.properties) {
|
||||
const propertyCount = Object.keys(node.properties).length;
|
||||
if (propertyCount > 0) {
|
||||
nodeHeight += propertyCount * 20 + 20;
|
||||
nodeWidth = Math.max(150, nodeWidth + 50);
|
||||
}
|
||||
}
|
||||
|
||||
const portX = node.x + nodeWidth / 2;
|
||||
const portY = portType === 'input'
|
||||
? node.y - 8
|
||||
: node.y + nodeHeight + 8;
|
||||
|
||||
return { x: portX, y: portY };
|
||||
};
|
||||
|
||||
const getNodeEdgePortPosition = (nodeElement: HTMLElement, node: any, portType: 'input' | 'output') => {
|
||||
const nodeRect = nodeElement.getBoundingClientRect();
|
||||
const canvasRect = canvasAreaRef.value?.getBoundingClientRect();
|
||||
|
||||
if (!canvasRect) {
|
||||
return getCalculatedPortPosition(node, portType);
|
||||
}
|
||||
|
||||
// 计算节点在SVG坐标系中的实际大小和位置
|
||||
const nodeWidth = nodeRect.width / zoomLevel.value;
|
||||
const nodeHeight = nodeRect.height / zoomLevel.value;
|
||||
|
||||
// 端口位于节点的水平中心
|
||||
const portX = node.x + nodeWidth / 2;
|
||||
const portY = portType === 'input'
|
||||
? node.y - 5
|
||||
: node.y + nodeHeight + 5;
|
||||
|
||||
return { x: portX, y: portY };
|
||||
};
|
||||
|
||||
const startConnection = (event: MouseEvent, nodeId: string, portType: 'input' | 'output') => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
connectionState.isConnecting = true;
|
||||
connectionState.startNodeId = nodeId;
|
||||
connectionState.startPortType = portType;
|
||||
connectionState.currentMousePos = { x: event.clientX, y: event.clientY };
|
||||
|
||||
const startPos = getPortPosition(nodeId, portType);
|
||||
if (startPos) {
|
||||
connectionState.startPortPos = startPos;
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onConnectionDrag);
|
||||
document.addEventListener('mouseup', onConnectionEnd);
|
||||
|
||||
if (canvasAreaRef.value) {
|
||||
canvasAreaRef.value.classList.add('connecting');
|
||||
}
|
||||
};
|
||||
|
||||
// 连接拖拽
|
||||
const onConnectionDrag = (event: MouseEvent) => {
|
||||
if (!connectionState.isConnecting || !connectionState.startNodeId || !connectionState.startPortType) return;
|
||||
|
||||
connectionState.currentMousePos = { x: event.clientX, y: event.clientY };
|
||||
|
||||
const svgPos = clientToSVGCoordinates(event.clientX, event.clientY);
|
||||
const startPos = getPortPosition(connectionState.startNodeId, connectionState.startPortType);
|
||||
|
||||
if (startPos && svgPos) {
|
||||
const controlOffset = Math.abs(svgPos.y - startPos.y) * 0.5;
|
||||
let path: string;
|
||||
|
||||
if (connectionState.startPortType === 'output') {
|
||||
path = `M ${startPos.x} ${startPos.y} C ${startPos.x} ${startPos.y + controlOffset} ${svgPos.x} ${svgPos.y - controlOffset} ${svgPos.x} ${svgPos.y}`;
|
||||
} else {
|
||||
path = `M ${startPos.x} ${startPos.y} C ${startPos.x} ${startPos.y - controlOffset} ${svgPos.x} ${svgPos.y + controlOffset} ${svgPos.x} ${svgPos.y}`;
|
||||
}
|
||||
|
||||
if ('tempPath' in connectionState) {
|
||||
(connectionState as any).tempPath = path;
|
||||
}
|
||||
}
|
||||
const targetPort = findTargetPort(event.clientX, event.clientY);
|
||||
if (targetPort && targetPort.nodeId !== connectionState.startNodeId) {
|
||||
connectionState.hoveredPort = targetPort;
|
||||
} else {
|
||||
connectionState.hoveredPort = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 结束连接
|
||||
const onConnectionEnd = (event: MouseEvent) => {
|
||||
if (!connectionState.isConnecting) return;
|
||||
|
||||
// 检查是否落在有效的端口上
|
||||
const targetPort = findTargetPort(event.clientX, event.clientY);
|
||||
|
||||
if (targetPort && connectionState.startNodeId && connectionState.startPortType) {
|
||||
const canConnectResult = canConnect(
|
||||
connectionState.startNodeId,
|
||||
connectionState.startPortType,
|
||||
targetPort.nodeId,
|
||||
targetPort.portType
|
||||
);
|
||||
|
||||
if (canConnectResult) {
|
||||
let parentId: string, childId: string;
|
||||
|
||||
if (connectionState.startPortType === 'output') {
|
||||
parentId = connectionState.startNodeId;
|
||||
childId = targetPort.nodeId;
|
||||
} else {
|
||||
parentId = targetPort.nodeId;
|
||||
childId = connectionState.startNodeId;
|
||||
}
|
||||
|
||||
createConnection(parentId, childId);
|
||||
}
|
||||
}
|
||||
|
||||
// 清理连接状态
|
||||
cancelConnection();
|
||||
};
|
||||
|
||||
// 取消连接
|
||||
const cancelConnection = () => {
|
||||
connectionState.isConnecting = false;
|
||||
connectionState.startNodeId = null;
|
||||
connectionState.startPortType = null;
|
||||
connectionState.currentMousePos = null;
|
||||
connectionState.startPortPos = null;
|
||||
connectionState.hoveredPort = null;
|
||||
|
||||
if ('tempPath' in connectionState) {
|
||||
(connectionState as any).tempPath = '';
|
||||
}
|
||||
|
||||
document.removeEventListener('mousemove', onConnectionDrag);
|
||||
document.removeEventListener('mouseup', onConnectionEnd);
|
||||
|
||||
if (canvasAreaRef.value) {
|
||||
canvasAreaRef.value.classList.remove('connecting');
|
||||
}
|
||||
// 清除画布内的拖拽目标样式
|
||||
if (canvasAreaRef.value) {
|
||||
const allPorts = canvasAreaRef.value.querySelectorAll('.port.drag-target');
|
||||
allPorts.forEach(port => port.classList.remove('drag-target'));
|
||||
}
|
||||
};
|
||||
|
||||
const clientToSVGCoordinates = (clientX: number, clientY: number) => {
|
||||
if (!canvasAreaRef.value) return null;
|
||||
|
||||
try {
|
||||
// 获取canvas容器的边界
|
||||
const canvasRect = canvasAreaRef.value.getBoundingClientRect();
|
||||
|
||||
// 转换为相对于canvas的坐标
|
||||
const canvasX = clientX - canvasRect.left;
|
||||
const canvasY = clientY - canvasRect.top;
|
||||
|
||||
// 撤销SVG的transform,转换为SVG坐标
|
||||
// SVG transform: translate(panX, panY) scale(zoomLevel)
|
||||
const svgX = (canvasX - panX.value) / zoomLevel.value;
|
||||
const svgY = (canvasY - panY.value) / zoomLevel.value;
|
||||
|
||||
return { x: svgX, y: svgY };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 查找目标端口
|
||||
const findTargetPort = (clientX: number, clientY: number) => {
|
||||
if (!canvasAreaRef.value) return null;
|
||||
|
||||
try {
|
||||
const elementAtPoint = document.elementFromPoint(clientX, clientY);
|
||||
if (elementAtPoint?.classList.contains('port') && canvasAreaRef.value.contains(elementAtPoint)) {
|
||||
return getPortInfo(elementAtPoint as HTMLElement);
|
||||
}
|
||||
} catch (error) {
|
||||
// 查询出错时静默处理
|
||||
}
|
||||
|
||||
const allPorts = canvasAreaRef.value.querySelectorAll('.port');
|
||||
for (const port of allPorts) {
|
||||
const rect = port.getBoundingClientRect();
|
||||
const margin = 10;
|
||||
|
||||
if (clientX >= rect.left - margin && clientX <= rect.right + margin &&
|
||||
clientY >= rect.top - margin && clientY <= rect.bottom + margin) {
|
||||
return getPortInfo(port as HTMLElement);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// 从端口元素获取端口信息
|
||||
const getPortInfo = (portElement: HTMLElement) => {
|
||||
const nodeElement = portElement.closest('.tree-node');
|
||||
if (!nodeElement) return null;
|
||||
|
||||
const nodeId = nodeElement.getAttribute('data-node-id');
|
||||
const portType = portElement.classList.contains('port-input') ? 'input' : 'output' as 'input' | 'output';
|
||||
|
||||
return nodeId ? { nodeId, portType } : null;
|
||||
};
|
||||
|
||||
// 端口悬停处理
|
||||
const onPortHover = (nodeId: string, portType: 'input' | 'output') => {
|
||||
if (connectionState.isConnecting && connectionState.startNodeId !== nodeId) {
|
||||
connectionState.hoveredPort = { nodeId, portType };
|
||||
|
||||
if (canConnect(connectionState.startNodeId!, connectionState.startPortType!, nodeId, portType)) {
|
||||
// 在画布区域内查找端口元素
|
||||
if (canvasAreaRef.value) {
|
||||
const portElement = canvasAreaRef.value.querySelector(`[data-node-id="${nodeId}"] .port.port-${portType}`);
|
||||
if (portElement) {
|
||||
portElement.classList.add('drag-target');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onPortLeave = () => {
|
||||
if (connectionState.isConnecting) {
|
||||
connectionState.hoveredPort = null;
|
||||
// 清除画布内的拖拽目标样式
|
||||
if (canvasAreaRef.value) {
|
||||
const allPorts = canvasAreaRef.value.querySelectorAll('.port.drag-target');
|
||||
allPorts.forEach(port => port.classList.remove('drag-target'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 验证连接目标是否有效
|
||||
const isValidConnectionTarget = (nodeId: string, portType: 'input' | 'output') => {
|
||||
if (!connectionState.isConnecting || !connectionState.startNodeId || connectionState.startNodeId === nodeId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return canConnect(connectionState.startNodeId, connectionState.startPortType!, nodeId, portType);
|
||||
};
|
||||
|
||||
// 检查是否可以连接
|
||||
const canConnect = (sourceNodeId: string, sourcePortType: string, targetNodeId: string, targetPortType: string) => {
|
||||
if (sourceNodeId === targetNodeId) return false;
|
||||
if (sourcePortType === targetPortType) return false;
|
||||
|
||||
let parentNodeId: string, childNodeId: string;
|
||||
|
||||
if (sourcePortType === 'output') {
|
||||
parentNodeId = sourceNodeId;
|
||||
childNodeId = targetNodeId;
|
||||
} else {
|
||||
parentNodeId = targetNodeId;
|
||||
childNodeId = sourceNodeId;
|
||||
}
|
||||
|
||||
const childNode = treeNodes.value.find(n => n.id === childNodeId);
|
||||
if (childNode && childNode.parent && childNode.parent !== parentNodeId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentNode = treeNodes.value.find(n => n.id === parentNodeId);
|
||||
if (!parentNode || !parentNode.canHaveChildren) return false;
|
||||
if (!childNode || !childNode.canHaveParent) return false;
|
||||
|
||||
// 检查子节点数量限制
|
||||
if (parentNode.maxChildren !== undefined) {
|
||||
const currentChildrenCount = parentNode.children ? parentNode.children.length : 0;
|
||||
if (currentChildrenCount >= parentNode.maxChildren) {
|
||||
return false; // 已达到最大子节点数量
|
||||
}
|
||||
}
|
||||
|
||||
// 检查根节点限制:根节点不能有父节点
|
||||
if (childNode.type === 'root') {
|
||||
return false; // 根节点不能作为其他节点的子节点
|
||||
}
|
||||
|
||||
// 检查是否只能有一个根节点
|
||||
if (parentNode.type === 'root') {
|
||||
// 根节点只能连接一个子节点
|
||||
const rootNodes = treeNodes.value.filter(n => n.type === 'root');
|
||||
if (rootNodes.length > 1) {
|
||||
return false; // 不能有多个根节点
|
||||
}
|
||||
}
|
||||
|
||||
if (wouldCreateCycle(parentNodeId, childNodeId)) return false;
|
||||
if (isDescendant(childNodeId, parentNodeId)) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// 检查是否会创建循环
|
||||
const wouldCreateCycle = (parentId: string, childId: string) => {
|
||||
return isDescendant(parentId, childId);
|
||||
};
|
||||
|
||||
const isDescendant = (ancestorId: string, descendantId: string): boolean => {
|
||||
const visited = new Set<string>();
|
||||
|
||||
function checkPath(currentId: string): boolean {
|
||||
if (currentId === ancestorId) return true;
|
||||
if (visited.has(currentId)) return false;
|
||||
|
||||
visited.add(currentId);
|
||||
|
||||
const currentNode = treeNodes.value.find(n => n.id === currentId);
|
||||
if (currentNode?.children) {
|
||||
for (const childId of currentNode.children) {
|
||||
if (checkPath(childId)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return checkPath(descendantId);
|
||||
};
|
||||
|
||||
// 创建连接
|
||||
const createConnection = (parentId: string, childId: string) => {
|
||||
const parentNode = treeNodes.value.find(n => n.id === parentId);
|
||||
const childNode = treeNodes.value.find(n => n.id === childId);
|
||||
|
||||
if (!parentNode || !childNode) return;
|
||||
|
||||
// 移除子节点的旧父子关系
|
||||
if (childNode.parent) {
|
||||
const oldParent = treeNodes.value.find(n => n.id === childNode.parent);
|
||||
if (oldParent) {
|
||||
const index = oldParent.children.indexOf(childId);
|
||||
if (index > -1) {
|
||||
oldParent.children.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 建立新的父子关系
|
||||
childNode.parent = parentId;
|
||||
if (!parentNode.children.includes(childId)) {
|
||||
parentNode.children.push(childId);
|
||||
}
|
||||
|
||||
updateConnections();
|
||||
};
|
||||
|
||||
// 改进的连接线更新方法
|
||||
const updateConnections = () => {
|
||||
// 立即清空现有连接
|
||||
connections.value.length = 0;
|
||||
|
||||
// 创建新的连接数据
|
||||
const newConnections: Connection[] = [];
|
||||
|
||||
// 遍历所有节点建立连接
|
||||
treeNodes.value.forEach(node => {
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach(childId => {
|
||||
const childNode = treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode) {
|
||||
// 尝试获取端口位置
|
||||
const parentPos = getPortPosition(node.id, 'output');
|
||||
const childPos = getPortPosition(childId, 'input');
|
||||
|
||||
if (parentPos && childPos) {
|
||||
// 计算贝塞尔曲线路径
|
||||
const deltaY = Math.abs(childPos.y - parentPos.y);
|
||||
const controlOffset = Math.max(30, Math.min(deltaY * 0.5, 80));
|
||||
|
||||
const path = `M ${parentPos.x} ${parentPos.y} C ${parentPos.x} ${parentPos.y + controlOffset} ${childPos.x} ${childPos.y - controlOffset} ${childPos.x} ${childPos.y}`;
|
||||
|
||||
newConnections.push({
|
||||
id: `${node.id}-${childId}`,
|
||||
sourceId: node.id,
|
||||
targetId: childId,
|
||||
path: path,
|
||||
active: false
|
||||
});
|
||||
} else {
|
||||
// 如果无法获取实际位置,使用计算位置作为后备
|
||||
const fallbackParentPos = getCalculatedPortPosition(node, 'output');
|
||||
const fallbackChildPos = getCalculatedPortPosition(childNode, 'input');
|
||||
|
||||
const deltaY = Math.abs(fallbackChildPos.y - fallbackParentPos.y);
|
||||
const controlOffset = Math.max(30, Math.min(deltaY * 0.5, 80));
|
||||
|
||||
const path = `M ${fallbackParentPos.x} ${fallbackParentPos.y} C ${fallbackParentPos.x} ${fallbackParentPos.y + controlOffset} ${fallbackChildPos.x} ${fallbackChildPos.y - controlOffset} ${fallbackChildPos.x} ${fallbackChildPos.y}`;
|
||||
|
||||
newConnections.push({
|
||||
id: `${node.id}-${childId}`,
|
||||
sourceId: node.id,
|
||||
targetId: childId,
|
||||
path: path,
|
||||
active: false
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 批量更新连接
|
||||
connections.value.push(...newConnections);
|
||||
|
||||
// 如果有DOM元素,进行二次精确更新
|
||||
if (canvasAreaRef.value) {
|
||||
setTimeout(() => {
|
||||
// 二次更新,使用实际DOM位置
|
||||
const updatedConnections: Connection[] = [];
|
||||
|
||||
treeNodes.value.forEach(node => {
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children.forEach(childId => {
|
||||
const childNode = treeNodes.value.find(n => n.id === childId);
|
||||
if (childNode) {
|
||||
const parentPos = getPortPosition(node.id, 'output');
|
||||
const childPos = getPortPosition(childId, 'input');
|
||||
|
||||
if (parentPos && childPos) {
|
||||
const deltaY = Math.abs(childPos.y - parentPos.y);
|
||||
const controlOffset = Math.max(30, Math.min(deltaY * 0.5, 80));
|
||||
|
||||
const path = `M ${parentPos.x} ${parentPos.y} C ${parentPos.x} ${parentPos.y + controlOffset} ${childPos.x} ${childPos.y - controlOffset} ${childPos.x} ${childPos.y}`;
|
||||
|
||||
updatedConnections.push({
|
||||
id: `${node.id}-${childId}`,
|
||||
sourceId: node.id,
|
||||
targetId: childId,
|
||||
path: path,
|
||||
active: false
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 如果二次更新得到了有效结果,替换连接数据
|
||||
if (updatedConnections.length > 0) {
|
||||
connections.value.length = 0;
|
||||
connections.value.push(...updatedConnections);
|
||||
}
|
||||
}, 100); // 100ms延迟,确保DOM完全渲染
|
||||
}
|
||||
};
|
||||
|
||||
// 删除连接线
|
||||
const removeConnection = (connectionId: string) => {
|
||||
const connection = connections.value.find(conn => conn.id === connectionId);
|
||||
if (!connection) return;
|
||||
|
||||
const parentNode = treeNodes.value.find(n => n.id === connection.sourceId);
|
||||
const childNode = treeNodes.value.find(n => n.id === connection.targetId);
|
||||
|
||||
if (parentNode && childNode) {
|
||||
// 从父节点的children中移除
|
||||
const index = parentNode.children.indexOf(connection.targetId);
|
||||
if (index > -1) {
|
||||
parentNode.children.splice(index, 1);
|
||||
}
|
||||
|
||||
// 清除子节点的parent
|
||||
childNode.parent = undefined;
|
||||
|
||||
// 更新连接线
|
||||
updateConnections();
|
||||
}
|
||||
};
|
||||
|
||||
// 连接线点击事件处理
|
||||
const onConnectionClick = (event: MouseEvent, connectionId: string) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// 询问用户是否要删除连接
|
||||
if (confirm('确定要删除这条连接线吗?')) {
|
||||
removeConnection(connectionId);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getPortPosition,
|
||||
startConnection,
|
||||
cancelConnection,
|
||||
updateConnections,
|
||||
removeConnection,
|
||||
onConnectionClick,
|
||||
onPortHover,
|
||||
onPortLeave,
|
||||
isValidConnectionTarget
|
||||
};
|
||||
}
|
||||
@@ -1,538 +0,0 @@
|
||||
import { Ref, ref, watch } from 'vue';
|
||||
import { TreeNode, Connection } from '../types';
|
||||
|
||||
interface FileOperationOptions {
|
||||
treeNodes: Ref<TreeNode[]>;
|
||||
selectedNodeId: Ref<string | null>;
|
||||
connections: Ref<Connection[]>;
|
||||
tempConnection: Ref<{ path: string }>;
|
||||
showExportModal: Ref<boolean>;
|
||||
codeGeneration?: {
|
||||
createTreeFromConfig: (config: any) => TreeNode[];
|
||||
};
|
||||
updateConnections?: () => void;
|
||||
blackboardOperations?: {
|
||||
getBlackboardVariables: () => any[];
|
||||
loadBlackboardVariables: (variables: any[]) => void;
|
||||
clearBlackboard: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface FileData {
|
||||
nodes: TreeNode[];
|
||||
connections: Connection[];
|
||||
blackboard?: any[];
|
||||
metadata: {
|
||||
name: string;
|
||||
created: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function useFileOperations(options: FileOperationOptions) {
|
||||
const {
|
||||
treeNodes,
|
||||
selectedNodeId,
|
||||
connections,
|
||||
tempConnection,
|
||||
showExportModal,
|
||||
codeGeneration,
|
||||
updateConnections,
|
||||
blackboardOperations
|
||||
} = options;
|
||||
|
||||
const hasUnsavedChanges = ref(false);
|
||||
const lastSavedState = ref<string>('');
|
||||
const currentFileName = ref('');
|
||||
const currentFilePath = ref('');
|
||||
|
||||
const updateUnsavedStatus = () => {
|
||||
const currentState = JSON.stringify({
|
||||
nodes: treeNodes.value,
|
||||
connections: connections.value
|
||||
});
|
||||
hasUnsavedChanges.value = currentState !== lastSavedState.value;
|
||||
};
|
||||
|
||||
watch([treeNodes, connections], updateUnsavedStatus, { deep: true });
|
||||
|
||||
const markAsSaved = () => {
|
||||
const currentState = JSON.stringify({
|
||||
nodes: treeNodes.value,
|
||||
connections: connections.value
|
||||
});
|
||||
lastSavedState.value = currentState;
|
||||
hasUnsavedChanges.value = false;
|
||||
};
|
||||
|
||||
const setCurrentFile = (fileName: string, filePath: string = '') => {
|
||||
currentFileName.value = fileName;
|
||||
currentFilePath.value = filePath;
|
||||
markAsSaved();
|
||||
};
|
||||
|
||||
const clearCurrentFile = () => {
|
||||
currentFileName.value = '';
|
||||
currentFilePath.value = '';
|
||||
};
|
||||
|
||||
const exportBehaviorTreeData = (): FileData => {
|
||||
const data: FileData = {
|
||||
nodes: treeNodes.value,
|
||||
connections: connections.value,
|
||||
metadata: {
|
||||
name: currentFileName.value || 'untitled',
|
||||
created: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
}
|
||||
};
|
||||
|
||||
// 包含黑板数据
|
||||
if (blackboardOperations) {
|
||||
const blackboardVariables = blackboardOperations.getBlackboardVariables();
|
||||
if (blackboardVariables.length > 0) {
|
||||
data.blackboard = blackboardVariables;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const showMessage = (message: string, type: 'success' | 'error' = 'success') => {
|
||||
const toast = document.createElement('div');
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 20px;
|
||||
background: ${type === 'success' ? '#4caf50' : '#f44336'};
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
z-index: 10001;
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
transition: all 0.3s ease;
|
||||
`;
|
||||
toast.textContent = message;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '1';
|
||||
toast.style.transform = 'translateX(0)';
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(toast)) {
|
||||
document.body.removeChild(toast);
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const sendToMain = (message: string, data: any): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
Editor.Message.request('cocos-ecs-extension', message, data)
|
||||
.then((result) => {
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const checkUnsavedChanges = (): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
if (!hasUnsavedChanges.value) {
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = confirm(
|
||||
'当前行为树有未保存的更改,是否要保存?\n\n' +
|
||||
'点击"确定"保存更改\n' +
|
||||
'点击"取消"丢弃更改'
|
||||
);
|
||||
|
||||
if (result) {
|
||||
saveBehaviorTree().then(() => {
|
||||
resolve(true);
|
||||
}).catch(() => {
|
||||
resolve(false);
|
||||
});
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const newBehaviorTree = async () => {
|
||||
const canProceed = await checkUnsavedChanges();
|
||||
if (canProceed) {
|
||||
treeNodes.value = [];
|
||||
selectedNodeId.value = null;
|
||||
connections.value = [];
|
||||
tempConnection.value.path = '';
|
||||
|
||||
// 清空黑板
|
||||
if (blackboardOperations) {
|
||||
blackboardOperations.clearBlackboard();
|
||||
}
|
||||
|
||||
clearCurrentFile();
|
||||
markAsSaved();
|
||||
}
|
||||
};
|
||||
|
||||
const saveBehaviorTree = async (): Promise<boolean> => {
|
||||
if (currentFilePath.value) {
|
||||
return await saveToCurrentFile();
|
||||
} else {
|
||||
return await saveAsBehaviorTree();
|
||||
}
|
||||
};
|
||||
|
||||
const saveToCurrentFile = async (): Promise<boolean> => {
|
||||
if (!currentFilePath.value) {
|
||||
return await saveAsBehaviorTree();
|
||||
}
|
||||
|
||||
try {
|
||||
const data = exportBehaviorTreeData();
|
||||
const jsonString = JSON.stringify(data, null, 2);
|
||||
|
||||
await sendToMain('overwrite-behavior-tree-file', {
|
||||
filePath: currentFilePath.value,
|
||||
content: jsonString
|
||||
});
|
||||
|
||||
markAsSaved();
|
||||
showMessage('保存成功!');
|
||||
return true;
|
||||
} catch (error) {
|
||||
showMessage('保存失败: ' + error, 'error');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveAsBehaviorTree = async (): Promise<boolean> => {
|
||||
try {
|
||||
const data = exportBehaviorTreeData();
|
||||
const jsonString = JSON.stringify(data, null, 2);
|
||||
|
||||
const result = await Editor.Dialog.save({
|
||||
title: '保存行为树文件',
|
||||
filters: [
|
||||
{ name: '行为树文件', extensions: ['bt.json', 'json'] },
|
||||
{ name: '所有文件', extensions: ['*'] }
|
||||
]
|
||||
});
|
||||
|
||||
if (result.canceled || !result.filePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fs = require('fs-extra');
|
||||
await fs.writeFile(result.filePath, jsonString);
|
||||
|
||||
const path = require('path');
|
||||
const fileName = path.basename(result.filePath, path.extname(result.filePath));
|
||||
setCurrentFile(fileName, result.filePath);
|
||||
showMessage(`保存成功!文件: ${result.filePath}`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
showMessage('另存为失败: ' + error, 'error');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveToFile = async (fileName: string, jsonString: string): Promise<boolean> => {
|
||||
try {
|
||||
await sendToMain('create-behavior-tree-from-editor', {
|
||||
fileName: fileName + '.json',
|
||||
content: jsonString
|
||||
});
|
||||
|
||||
setCurrentFile(fileName, `assets/${fileName}.bt.json`);
|
||||
showMessage(`保存成功!文件名: ${fileName}.json`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
showMessage('保存失败: ' + error, 'error');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getFileNameFromUser = (): Promise<string | null> => {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10000;
|
||||
`;
|
||||
|
||||
const dialog = document.createElement('div');
|
||||
dialog.style.cssText = `
|
||||
background: #2d2d2d;
|
||||
color: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
min-width: 300px;
|
||||
`;
|
||||
|
||||
dialog.innerHTML = `
|
||||
<h3 style="margin: 0 0 15px 0; color: #ffffff;">保存行为树</h3>
|
||||
<p style="margin: 0 0 15px 0; color: #cccccc;">请输入文件名(不含扩展名):</p>
|
||||
<input type="text" id="filename-input" value="${currentFileName.value || 'behavior_tree'}"
|
||||
style="width: 100%; padding: 8px; border: 1px solid #555; background: #1a1a1a; color: #ffffff; border-radius: 4px; margin-bottom: 15px;">
|
||||
<div style="text-align: right;">
|
||||
<button id="cancel-btn" style="padding: 8px 16px; margin-right: 8px; background: #555; color: #fff; border: none; border-radius: 4px; cursor: pointer;">取消</button>
|
||||
<button id="save-btn" style="padding: 8px 16px; background: #007acc; color: #fff; border: none; border-radius: 4px; cursor: pointer;">保存</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.appendChild(dialog);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const input = dialog.querySelector('#filename-input') as HTMLInputElement;
|
||||
const saveBtn = dialog.querySelector('#save-btn') as HTMLButtonElement;
|
||||
const cancelBtn = dialog.querySelector('#cancel-btn') as HTMLButtonElement;
|
||||
|
||||
input.focus();
|
||||
input.select();
|
||||
|
||||
const cleanup = () => {
|
||||
document.body.removeChild(overlay);
|
||||
};
|
||||
|
||||
saveBtn.onclick = () => {
|
||||
const fileName = input.value.trim();
|
||||
cleanup();
|
||||
resolve(fileName || null);
|
||||
};
|
||||
|
||||
cancelBtn.onclick = () => {
|
||||
cleanup();
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
input.onkeydown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const fileName = input.value.trim();
|
||||
cleanup();
|
||||
resolve(fileName || null);
|
||||
} else if (e.key === 'Escape') {
|
||||
cleanup();
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const loadFileContent = (fileData: any, filePath: string = '') => {
|
||||
try {
|
||||
if (!fileData) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parsedData = fileData;
|
||||
|
||||
if (fileData.rawContent) {
|
||||
try {
|
||||
parsedData = JSON.parse(fileData.rawContent);
|
||||
} catch (e) {
|
||||
parsedData = {
|
||||
nodes: [],
|
||||
connections: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedData.nodes && Array.isArray(parsedData.nodes)) {
|
||||
treeNodes.value = parsedData.nodes.map((node: any) => ({
|
||||
...node,
|
||||
x: node.x || 0,
|
||||
y: node.y || 0,
|
||||
children: node.children || [],
|
||||
properties: node.properties || {},
|
||||
canHaveChildren: node.canHaveChildren !== false,
|
||||
canHaveParent: node.canHaveParent !== false,
|
||||
hasError: node.hasError || false
|
||||
}));
|
||||
} else if (parsedData.tree) {
|
||||
const treeNode = parsedData.tree;
|
||||
const nodes = [treeNode];
|
||||
|
||||
const extractNodes = (node: any): any[] => {
|
||||
const allNodes = [node];
|
||||
if (node.children && Array.isArray(node.children)) {
|
||||
node.children.forEach((child: any) => {
|
||||
if (typeof child === 'object') {
|
||||
allNodes.push(...extractNodes(child));
|
||||
}
|
||||
});
|
||||
}
|
||||
return allNodes;
|
||||
};
|
||||
|
||||
const allNodes = extractNodes(treeNode);
|
||||
treeNodes.value = allNodes.map((node: any, index: number) => ({
|
||||
...node,
|
||||
x: node.x || (300 + index * 150),
|
||||
y: node.y || (100 + Math.floor(index / 3) * 200),
|
||||
children: Array.isArray(node.children)
|
||||
? node.children.filter((child: any) => typeof child === 'string')
|
||||
: [],
|
||||
properties: node.properties || {},
|
||||
canHaveChildren: true,
|
||||
canHaveParent: node.id !== 'root',
|
||||
hasError: false
|
||||
}));
|
||||
} else {
|
||||
treeNodes.value = [];
|
||||
}
|
||||
|
||||
if (parsedData.connections && Array.isArray(parsedData.connections)) {
|
||||
connections.value = parsedData.connections.map((conn: any) => ({
|
||||
id: conn.id || Math.random().toString(36).substr(2, 9),
|
||||
sourceId: conn.sourceId,
|
||||
targetId: conn.targetId,
|
||||
path: conn.path || '',
|
||||
active: conn.active || false
|
||||
}));
|
||||
} else {
|
||||
connections.value = [];
|
||||
}
|
||||
|
||||
if (fileData._fileInfo) {
|
||||
const fileName = fileData._fileInfo.fileName || 'untitled';
|
||||
const fullPath = fileData._fileInfo.filePath || filePath;
|
||||
setCurrentFile(fileName, fullPath);
|
||||
} else if (parsedData.metadata?.name) {
|
||||
setCurrentFile(parsedData.metadata.name, filePath);
|
||||
} else {
|
||||
setCurrentFile('untitled', filePath);
|
||||
}
|
||||
|
||||
// 加载黑板数据
|
||||
if (blackboardOperations && parsedData.blackboard && Array.isArray(parsedData.blackboard)) {
|
||||
blackboardOperations.loadBlackboardVariables(parsedData.blackboard);
|
||||
}
|
||||
|
||||
selectedNodeId.value = null;
|
||||
tempConnection.value.path = '';
|
||||
|
||||
if (updateConnections) {
|
||||
setTimeout(() => {
|
||||
updateConnections();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('文件加载失败:', error);
|
||||
showMessage('文件加载失败: ' + error, 'error');
|
||||
treeNodes.value = [];
|
||||
connections.value = [];
|
||||
selectedNodeId.value = null;
|
||||
setCurrentFile('untitled', '');
|
||||
}
|
||||
};
|
||||
|
||||
const loadBehaviorTree = async () => {
|
||||
const canProceed = await checkUnsavedChanges();
|
||||
if (!canProceed) return;
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json,.bt';
|
||||
input.onchange = (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
try {
|
||||
const configText = event.target?.result as string;
|
||||
const config = JSON.parse(configText);
|
||||
|
||||
if (codeGeneration) {
|
||||
const newNodes = codeGeneration.createTreeFromConfig(config);
|
||||
treeNodes.value = newNodes;
|
||||
selectedNodeId.value = null;
|
||||
|
||||
if (config.connections && Array.isArray(config.connections)) {
|
||||
connections.value = config.connections.map((conn: any) => ({
|
||||
id: conn.id,
|
||||
sourceId: conn.sourceId,
|
||||
targetId: conn.targetId,
|
||||
path: conn.path || '',
|
||||
active: conn.active || false
|
||||
}));
|
||||
} else {
|
||||
connections.value = [];
|
||||
}
|
||||
|
||||
tempConnection.value.path = '';
|
||||
|
||||
// 加载黑板数据
|
||||
if (blackboardOperations && config.blackboard && Array.isArray(config.blackboard)) {
|
||||
blackboardOperations.loadBlackboardVariables(config.blackboard);
|
||||
}
|
||||
|
||||
const fileName = file.name.replace(/\.(json|bt)$/, '');
|
||||
setCurrentFile(fileName, '');
|
||||
|
||||
setTimeout(() => {
|
||||
if (updateConnections) {
|
||||
updateConnections();
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
showMessage('代码生成器未初始化', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('配置文件格式错误', 'error');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
const exportConfig = () => {
|
||||
showExportModal.value = true;
|
||||
};
|
||||
|
||||
return {
|
||||
newBehaviorTree,
|
||||
saveBehaviorTree,
|
||||
saveAsBehaviorTree,
|
||||
loadBehaviorTree,
|
||||
loadFileContent,
|
||||
exportConfig,
|
||||
hasUnsavedChanges,
|
||||
markAsSaved,
|
||||
setCurrentFile,
|
||||
clearCurrentFile,
|
||||
currentFileName,
|
||||
currentFilePath
|
||||
};
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Ref } from 'vue';
|
||||
import { checkBehaviorTreeInstalled, installBehaviorTreeAI } from '../utils/installUtils';
|
||||
|
||||
/**
|
||||
* 安装管理
|
||||
*/
|
||||
export function useInstallation(
|
||||
checkingStatus: Ref<boolean>,
|
||||
isInstalled: Ref<boolean>,
|
||||
version: Ref<string | null>,
|
||||
isInstalling: Ref<boolean>
|
||||
) {
|
||||
|
||||
// 检查安装状态
|
||||
const checkInstallStatus = async () => {
|
||||
checkingStatus.value = true;
|
||||
try {
|
||||
const result = await checkBehaviorTreeInstalled(Editor.Project.path);
|
||||
isInstalled.value = result.installed;
|
||||
version.value = result.version;
|
||||
} catch (error) {
|
||||
console.error('检查AI系统安装状态失败:', error);
|
||||
isInstalled.value = false;
|
||||
version.value = null;
|
||||
} finally {
|
||||
checkingStatus.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理安装
|
||||
const handleInstall = async () => {
|
||||
isInstalling.value = true;
|
||||
try {
|
||||
const result = await installBehaviorTreeAI(Editor.Project.path);
|
||||
|
||||
if (result) {
|
||||
// 等待文件系统更新
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
await checkInstallStatus();
|
||||
|
||||
// 如果第一次检查失败,再次尝试
|
||||
if (!isInstalled.value) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await checkInstallStatus();
|
||||
}
|
||||
} else {
|
||||
console.error('AI系统安装失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('安装AI系统时发生错误:', error);
|
||||
} finally {
|
||||
isInstalling.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
checkInstallStatus,
|
||||
handleInstall
|
||||
};
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* 节点显示管理功能
|
||||
*/
|
||||
export function useNodeDisplay() {
|
||||
|
||||
// 检查节点是否有可见属性
|
||||
const hasVisibleProperties = (node: any) => {
|
||||
if (!node.properties) return false;
|
||||
return Object.keys(getVisibleProperties(node)).length > 0;
|
||||
};
|
||||
|
||||
// 获取可见属性
|
||||
const getVisibleProperties = (node: any) => {
|
||||
if (!node.properties) return {};
|
||||
|
||||
const visibleProps: any = {};
|
||||
for (const [key, prop] of Object.entries(node.properties)) {
|
||||
if (shouldShowProperty(prop as any, key)) {
|
||||
visibleProps[key] = prop;
|
||||
}
|
||||
}
|
||||
return visibleProps;
|
||||
};
|
||||
|
||||
// 判断属性是否应该显示
|
||||
const shouldShowProperty = (prop: any, key: string) => {
|
||||
// 总是显示这些重要属性
|
||||
const alwaysShow = ['abortType', 'repeatCount', 'priority'];
|
||||
if (alwaysShow.includes(key)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 对于其他属性,只在非默认值时显示
|
||||
if (prop.type === 'string' && prop.value && prop.value.trim() !== '') {
|
||||
return true;
|
||||
}
|
||||
if (prop.type === 'number' && prop.value !== 0 && prop.value !== -1) {
|
||||
return true;
|
||||
}
|
||||
if (prop.type === 'boolean' && prop.value === true) {
|
||||
return true;
|
||||
}
|
||||
if (prop.type === 'select' && prop.value !== 'None' && prop.value !== '') {
|
||||
return true;
|
||||
}
|
||||
if (prop.type === 'code' && prop.value && prop.value.trim() !== '' && prop.value !== '(context) => true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// 格式化属性值显示
|
||||
const formatPropertyValue = (prop: any) => {
|
||||
switch (prop.type) {
|
||||
case 'boolean':
|
||||
return prop.value ? '✓' : '✗';
|
||||
case 'number':
|
||||
return prop.value.toString();
|
||||
case 'select':
|
||||
return prop.value;
|
||||
case 'string':
|
||||
return prop.value.length > 15 ? prop.value.substring(0, 15) + '...' : prop.value;
|
||||
case 'code':
|
||||
const code = prop.value || '';
|
||||
if (code.length > 20) {
|
||||
// 尝试提取函数体的关键部分
|
||||
const bodyMatch = code.match(/=>\s*(.+)/) || code.match(/{\s*(.+?)\s*}/);
|
||||
if (bodyMatch) {
|
||||
const body = bodyMatch[1].trim();
|
||||
return body.length > 15 ? body.substring(0, 15) + '...' : body;
|
||||
}
|
||||
return code.substring(0, 20) + '...';
|
||||
}
|
||||
return code;
|
||||
default:
|
||||
return prop.value?.toString() || '';
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
hasVisibleProperties,
|
||||
getVisibleProperties,
|
||||
formatPropertyValue
|
||||
};
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
import { Ref, nextTick } from 'vue';
|
||||
import { TreeNode, Connection } from '../types';
|
||||
import { NodeTemplate } from '../data/nodeTemplates';
|
||||
import { createNodeFromTemplate } from '../utils/nodeUtils';
|
||||
import { getCanvasCoordinates } from '../utils/canvasUtils';
|
||||
|
||||
/**
|
||||
* 节点操作管理
|
||||
*/
|
||||
export function useNodeOperations(
|
||||
treeNodes: Ref<TreeNode[]>,
|
||||
selectedNodeId: Ref<string | null>,
|
||||
connections: Ref<Connection[]>,
|
||||
panX: Ref<number>,
|
||||
panY: Ref<number>,
|
||||
zoomLevel: Ref<number>,
|
||||
getNodeByIdLocal: (id: string) => TreeNode | undefined,
|
||||
updateConnections?: () => void
|
||||
) {
|
||||
|
||||
// 获取相对于画布的坐标(用于节点拖放等操作)
|
||||
const getCanvasCoords = (event: MouseEvent, canvasElement: HTMLElement | null) => {
|
||||
return getCanvasCoordinates(event, canvasElement, panX.value, panY.value, zoomLevel.value);
|
||||
};
|
||||
|
||||
// 拖拽事件处理
|
||||
const onNodeDragStart = (event: DragEvent, template: NodeTemplate) => {
|
||||
if (event.dataTransfer) {
|
||||
// 检查是否为条件节点,如果是则标记为条件拖拽
|
||||
const dragData = {
|
||||
...template,
|
||||
isConditionDrag: template.isDraggableCondition || false
|
||||
};
|
||||
event.dataTransfer.setData('application/json', JSON.stringify(dragData));
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
}
|
||||
};
|
||||
|
||||
const onCanvasDragOver = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
};
|
||||
|
||||
const onCanvasDrop = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
const templateData = event.dataTransfer?.getData('application/json');
|
||||
if (!templateData) return;
|
||||
|
||||
try {
|
||||
const dragData = JSON.parse(templateData);
|
||||
|
||||
// 如果是条件节点拖拽,阻止创建独立节点
|
||||
if (dragData.isConditionDrag || dragData.isDraggableCondition) {
|
||||
return; // 条件节点不能作为独立节点创建
|
||||
}
|
||||
|
||||
const template: NodeTemplate = dragData;
|
||||
const canvasElement = event.currentTarget as HTMLElement;
|
||||
const { x, y } = getCanvasCoords(event, canvasElement);
|
||||
|
||||
const newNode = createNodeFromTemplate(template, x, y);
|
||||
treeNodes.value.push(newNode);
|
||||
selectedNodeId.value = newNode.id;
|
||||
|
||||
} catch (error) {
|
||||
// 节点创建失败时静默处理
|
||||
}
|
||||
};
|
||||
|
||||
// 节点删除(递归删除子节点)
|
||||
const deleteNode = (nodeId: string) => {
|
||||
const deleteRecursive = (id: string) => {
|
||||
const node = getNodeByIdLocal(id);
|
||||
if (!node) return;
|
||||
|
||||
// 递归删除子节点
|
||||
node.children.forEach(childId => deleteRecursive(childId));
|
||||
|
||||
// 从父节点的children中移除
|
||||
if (node.parent) {
|
||||
const parent = getNodeByIdLocal(node.parent);
|
||||
if (parent) {
|
||||
const index = parent.children.indexOf(id);
|
||||
if (index > -1) {
|
||||
parent.children.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移除连接
|
||||
connections.value = connections.value.filter(conn =>
|
||||
conn.sourceId !== id && conn.targetId !== id
|
||||
);
|
||||
|
||||
// 从树中移除节点
|
||||
const nodeIndex = treeNodes.value.findIndex(n => n.id === id);
|
||||
if (nodeIndex > -1) {
|
||||
treeNodes.value.splice(nodeIndex, 1);
|
||||
}
|
||||
};
|
||||
|
||||
deleteRecursive(nodeId);
|
||||
|
||||
if (selectedNodeId.value === nodeId) {
|
||||
selectedNodeId.value = null;
|
||||
}
|
||||
|
||||
// 更新连接线
|
||||
if (updateConnections) {
|
||||
updateConnections();
|
||||
}
|
||||
};
|
||||
|
||||
// 通用的属性更新方法
|
||||
const setNestedProperty = (obj: any, path: string, value: any) => {
|
||||
const keys = path.split('.');
|
||||
let current = obj;
|
||||
|
||||
// 导航到目标属性的父对象
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
|
||||
current[key] = {};
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
// 设置最终值
|
||||
const finalKey = keys[keys.length - 1];
|
||||
current[finalKey] = value;
|
||||
};
|
||||
|
||||
// 节点属性更新
|
||||
const updateNodeProperty = (path: string, value: any) => {
|
||||
const selectedNode = selectedNodeId.value ? getNodeByIdLocal(selectedNodeId.value) : null;
|
||||
if (!selectedNode) return;
|
||||
|
||||
// 检查是否是条件节点的属性更新
|
||||
if (selectedNode.isConditionNode && selectedNode.parentDecorator) {
|
||||
// 条件节点的属性更新需要同步到装饰器
|
||||
updateConditionNodeProperty(selectedNode.parentDecorator, path, value);
|
||||
} else {
|
||||
// 普通节点的属性更新
|
||||
setNestedProperty(selectedNode, path, value);
|
||||
|
||||
// 强制触发响应式更新
|
||||
const nodeIndex = treeNodes.value.findIndex(n => n.id === selectedNode.id);
|
||||
if (nodeIndex > -1) {
|
||||
const newNodes = [...treeNodes.value];
|
||||
newNodes[nodeIndex] = { ...selectedNode };
|
||||
treeNodes.value = newNodes;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 更新条件节点属性到装饰器
|
||||
const updateConditionNodeProperty = (decoratorNode: TreeNode, path: string, value: any) => {
|
||||
// 解析属性路径,例如 "properties.variableName.value" -> "variableName"
|
||||
const pathParts = path.split('.');
|
||||
if (pathParts[0] === 'properties' && pathParts[2] === 'value') {
|
||||
const propertyName = pathParts[1];
|
||||
|
||||
// 直接更新装饰器的属性
|
||||
if (!decoratorNode.properties) {
|
||||
decoratorNode.properties = {};
|
||||
}
|
||||
decoratorNode.properties[propertyName] = value;
|
||||
|
||||
// 强制触发响应式更新
|
||||
const nodeIndex = treeNodes.value.findIndex(n => n.id === decoratorNode.id);
|
||||
if (nodeIndex > -1) {
|
||||
const newNodes = [...treeNodes.value];
|
||||
newNodes[nodeIndex] = { ...decoratorNode };
|
||||
treeNodes.value = newNodes;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getCanvasCoords,
|
||||
onNodeDragStart,
|
||||
onCanvasDragOver,
|
||||
onCanvasDrop,
|
||||
deleteNode,
|
||||
updateNodeProperty
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,180 +0,0 @@
|
||||
import { readFileSync } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { createApp, App, defineComponent } from 'vue';
|
||||
import { useBehaviorTreeEditor } from './composables/useBehaviorTreeEditor';
|
||||
import { EventManager } from './utils/EventManager';
|
||||
|
||||
// Vue应用实例
|
||||
let panelDataMap = new WeakMap<any, any>();
|
||||
|
||||
// 待处理的文件队列
|
||||
let pendingFileData: any = null;
|
||||
// Vue应用是否已挂载完成
|
||||
let vueAppMounted: boolean = false;
|
||||
// 存储面板实例,用于访问面板的DOM元素
|
||||
let currentPanelInstance: any = null;
|
||||
|
||||
/**
|
||||
* 面板定义
|
||||
*/
|
||||
const panelDefinition = {
|
||||
template: readFileSync(join(__dirname, '../../../static/template/behavior-tree/index.html'), 'utf-8'),
|
||||
|
||||
style: [
|
||||
readFileSync(join(__dirname, '../../../static/style/behavior-tree/base.css'), 'utf-8'),
|
||||
readFileSync(join(__dirname, '../../../static/style/behavior-tree/toolbar.css'), 'utf-8'),
|
||||
readFileSync(join(__dirname, '../../../static/style/behavior-tree/panels.css'), 'utf-8'),
|
||||
readFileSync(join(__dirname, '../../../static/style/behavior-tree/canvas.css'), 'utf-8'),
|
||||
readFileSync(join(__dirname, '../../../static/style/behavior-tree/nodes.css'), 'utf-8'),
|
||||
readFileSync(join(__dirname, '../../../static/style/behavior-tree/conditions.css'), 'utf-8'),
|
||||
readFileSync(join(__dirname, '../../../static/style/behavior-tree/modals.css'), 'utf-8')
|
||||
].join('\n'),
|
||||
|
||||
$: {
|
||||
app: '#behavior-tree-app',
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadBehaviorTreeFile(assetInfo: any) {
|
||||
try {
|
||||
const filePath = assetInfo?.file || assetInfo?.path;
|
||||
if (!filePath) {
|
||||
throw new Error('无法获取文件路径');
|
||||
}
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`文件不存在: ${filePath}`);
|
||||
}
|
||||
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
let fileContent: any;
|
||||
|
||||
try {
|
||||
fileContent = JSON.parse(content);
|
||||
} catch (parseError) {
|
||||
fileContent = {
|
||||
version: "1.0.0",
|
||||
type: "behavior-tree",
|
||||
tree: { id: "root", type: "sequence", children: [] }
|
||||
};
|
||||
}
|
||||
|
||||
const fileInfo = {
|
||||
...fileContent,
|
||||
_fileInfo: {
|
||||
fileName: path.basename(filePath, path.extname(filePath)),
|
||||
filePath: filePath
|
||||
}
|
||||
};
|
||||
|
||||
const notifyVueComponent = () => {
|
||||
const appContainer = currentPanelInstance?.$.app;
|
||||
|
||||
if (appContainer && vueAppMounted) {
|
||||
if (typeof (appContainer as any).loadFileContent === 'function') {
|
||||
(appContainer as any).loadFileContent(fileInfo);
|
||||
} else {
|
||||
const event = new CustomEvent('load-behavior-tree-file', { detail: fileInfo });
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
} else {
|
||||
pendingFileData = fileInfo;
|
||||
}
|
||||
};
|
||||
|
||||
notifyVueComponent();
|
||||
|
||||
if (pendingFileData) {
|
||||
setTimeout(() => {
|
||||
if (pendingFileData) {
|
||||
notifyVueComponent();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
return { success: true, message: '文件加载成功' };
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const event = new CustomEvent('file-load-error', { detail: { error: errorMessage } });
|
||||
document.dispatchEvent(event);
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
ready() {
|
||||
currentPanelInstance = this;
|
||||
|
||||
if (this.$.app) {
|
||||
try {
|
||||
const BehaviorTreeEditor = defineComponent({
|
||||
setup() {
|
||||
const editor = useBehaviorTreeEditor();
|
||||
return editor;
|
||||
},
|
||||
template: readFileSync(join(__dirname, '../../../static/template/behavior-tree/BehaviorTreeEditor.html'), 'utf-8')
|
||||
});
|
||||
|
||||
const app = createApp(BehaviorTreeEditor);
|
||||
|
||||
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('ui-');
|
||||
|
||||
app.config.errorHandler = (err, instance, info) => {
|
||||
console.error('[BehaviorTreePanel] Vue错误:', err, info);
|
||||
};
|
||||
|
||||
app.component('tree-node-item', defineComponent({
|
||||
props: ['node', 'level', 'getNodeByIdLocal'],
|
||||
emits: ['node-select'],
|
||||
template: `
|
||||
<div class="tree-node-item"
|
||||
:class="'level-' + level"
|
||||
@click="$emit('node-select', node)">
|
||||
<span class="node-icon">{{ node.icon || '●' }}</span>
|
||||
<span class="node-name">{{ node.name || node.type }}</span>
|
||||
<span class="node-type">{{ node.type }}</span>
|
||||
</div>
|
||||
`
|
||||
}));
|
||||
|
||||
app.mount(this.$.app);
|
||||
panelDataMap.set(this, app);
|
||||
vueAppMounted = true;
|
||||
|
||||
if (pendingFileData) {
|
||||
const event = new CustomEvent('load-behavior-tree-file', { detail: pendingFileData });
|
||||
document.dispatchEvent(event);
|
||||
pendingFileData = null;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[BehaviorTreePanel] 初始化失败:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 面板关闭时调用
|
||||
*/
|
||||
close() {
|
||||
try {
|
||||
const app = panelDataMap.get(this);
|
||||
if (app) {
|
||||
app.unmount();
|
||||
panelDataMap.delete(this);
|
||||
}
|
||||
|
||||
EventManager.getInstance().cleanup();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[BehaviorTreePanel] 清理资源时发生错误:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 导出面板定义 - 使用Editor.Panel.define()包装
|
||||
module.exports = Editor.Panel.define(panelDefinition);
|
||||
@@ -1,81 +0,0 @@
|
||||
import { PropertyDefinition } from '../data/nodeTemplates';
|
||||
|
||||
export interface TreeNode {
|
||||
id: string;
|
||||
type: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
x: number;
|
||||
y: number;
|
||||
children: string[];
|
||||
parent?: string;
|
||||
properties?: Record<string, any>; // 改为any以支持动态属性值
|
||||
canHaveChildren: boolean;
|
||||
canHaveParent: boolean;
|
||||
maxChildren?: number; // 最大子节点数量限制
|
||||
minChildren?: number; // 最小子节点数量要求
|
||||
hasError?: boolean;
|
||||
// 条件装饰器相关
|
||||
attachedCondition?: {
|
||||
type: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
};
|
||||
// 条件节点相关(用于虚拟条件节点)
|
||||
isConditionNode?: boolean;
|
||||
parentDecorator?: TreeNode;
|
||||
// 条件显示状态
|
||||
conditionExpanded?: boolean; // 条件是否展开显示详细信息
|
||||
}
|
||||
|
||||
export interface Connection {
|
||||
id: string;
|
||||
sourceId: string;
|
||||
targetId: string;
|
||||
path: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface DragState {
|
||||
isDraggingCanvas: boolean;
|
||||
isDraggingNode: boolean;
|
||||
isConnecting: boolean;
|
||||
dragStartX: number;
|
||||
dragStartY: number;
|
||||
dragNodeId: string | null;
|
||||
dragNodeStartX: number;
|
||||
dragNodeStartY: number;
|
||||
connectionStart: { nodeId: string; portType: string } | null;
|
||||
connectionEnd: { x: number; y: number };
|
||||
}
|
||||
|
||||
export interface InstallStatus {
|
||||
installed: boolean;
|
||||
version: string | null;
|
||||
packageExists: boolean;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
isValid: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ConnectionPort {
|
||||
nodeId: string;
|
||||
portType: string;
|
||||
}
|
||||
|
||||
export interface CanvasCoordinates {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface ConnectionState {
|
||||
isConnecting: boolean;
|
||||
startNodeId: string | null;
|
||||
startPortType: 'input' | 'output' | null;
|
||||
currentMousePos: { x: number; y: number } | null;
|
||||
startPortPos: { x: number; y: number } | null;
|
||||
hoveredPort: { nodeId: string; portType: 'input' | 'output' } | null;
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
/**
|
||||
* 事件管理器 - 统一处理面板的事件通信
|
||||
*/
|
||||
export class EventManager {
|
||||
private static instance: EventManager;
|
||||
private eventListeners: Map<string, EventListener[]> = new Map();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): EventManager {
|
||||
if (!EventManager.instance) {
|
||||
EventManager.instance = new EventManager();
|
||||
}
|
||||
return EventManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加事件监听器
|
||||
*/
|
||||
addEventListener(eventType: string, listener: EventListener): void {
|
||||
if (!this.eventListeners.has(eventType)) {
|
||||
this.eventListeners.set(eventType, []);
|
||||
}
|
||||
|
||||
const listeners = this.eventListeners.get(eventType)!;
|
||||
listeners.push(listener);
|
||||
|
||||
// 添加到DOM
|
||||
document.addEventListener(eventType, listener);
|
||||
|
||||
console.log(`[EventManager] 添加事件监听器: ${eventType}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件监听器
|
||||
*/
|
||||
removeEventListener(eventType: string, listener: EventListener): void {
|
||||
const listeners = this.eventListeners.get(eventType);
|
||||
if (listeners) {
|
||||
const index = listeners.indexOf(listener);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
document.removeEventListener(eventType, listener);
|
||||
console.log(`[EventManager] 移除事件监听器: ${eventType}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除特定类型的所有监听器
|
||||
*/
|
||||
removeAllListeners(eventType: string): void {
|
||||
const listeners = this.eventListeners.get(eventType);
|
||||
if (listeners) {
|
||||
listeners.forEach(listener => {
|
||||
document.removeEventListener(eventType, listener);
|
||||
});
|
||||
this.eventListeners.delete(eventType);
|
||||
console.log(`[EventManager] 移除所有 ${eventType} 事件监听器`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有事件监听器
|
||||
*/
|
||||
cleanup(): void {
|
||||
this.eventListeners.forEach((listeners, eventType) => {
|
||||
listeners.forEach(listener => {
|
||||
document.removeEventListener(eventType, listener);
|
||||
});
|
||||
});
|
||||
this.eventListeners.clear();
|
||||
console.log('[EventManager] 清理所有事件监听器');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到主进程
|
||||
*/
|
||||
static sendToMain(message: string, ...args: any[]): void {
|
||||
try {
|
||||
if (typeof (window as any).sendToMain === 'function') {
|
||||
(window as any).sendToMain(message, ...args);
|
||||
console.log(`[EventManager] 发送消息到主进程: ${message}`, args);
|
||||
} else {
|
||||
console.error('[EventManager] sendToMain 方法不可用');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[EventManager] 发送消息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发自定义事件
|
||||
*/
|
||||
static dispatch(eventType: string, detail?: any): void {
|
||||
try {
|
||||
const event = new CustomEvent(eventType, { detail });
|
||||
document.dispatchEvent(event);
|
||||
console.log(`[EventManager] 触发事件: ${eventType}`, detail);
|
||||
} catch (error) {
|
||||
console.error(`[EventManager] 触发事件失败: ${eventType}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import { CanvasCoordinates } from '../types';
|
||||
|
||||
/**
|
||||
* 获取相对于画布的坐标(考虑缩放和平移)
|
||||
*/
|
||||
export function getCanvasCoordinates(
|
||||
event: MouseEvent,
|
||||
canvasElement: HTMLElement | null,
|
||||
panX: number,
|
||||
panY: number,
|
||||
zoomLevel: number
|
||||
): CanvasCoordinates {
|
||||
if (!canvasElement) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
try {
|
||||
const rect = canvasElement.getBoundingClientRect();
|
||||
const x = (event.clientX - rect.left - panX) / zoomLevel;
|
||||
const y = (event.clientY - rect.top - panY) / zoomLevel;
|
||||
return { x, y };
|
||||
} catch (error) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算网格样式
|
||||
*/
|
||||
export function getGridStyle(panX: number, panY: number, zoomLevel: number) {
|
||||
const gridSize = 20 * zoomLevel;
|
||||
return {
|
||||
backgroundSize: `${gridSize}px ${gridSize}px`,
|
||||
backgroundPosition: `${panX % gridSize}px ${panY % gridSize}px`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算视图居中的平移值
|
||||
*/
|
||||
export function calculateCenterView(
|
||||
nodes: any[],
|
||||
canvasWidth: number,
|
||||
canvasHeight: number,
|
||||
zoomLevel: number
|
||||
): { panX: number; panY: number } {
|
||||
if (nodes.length === 0) {
|
||||
return { panX: 0, panY: 0 };
|
||||
}
|
||||
|
||||
// 计算所有节点的边界
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
|
||||
nodes.forEach(node => {
|
||||
minX = Math.min(minX, node.x);
|
||||
minY = Math.min(minY, node.y);
|
||||
maxX = Math.max(maxX, node.x + 150);
|
||||
maxY = Math.max(maxY, node.y + 100);
|
||||
});
|
||||
|
||||
// 计算中心点
|
||||
const centerX = (minX + maxX) / 2;
|
||||
const centerY = (minY + maxY) / 2;
|
||||
|
||||
// 设置平移,使内容居中
|
||||
const panX = canvasWidth / 2 - centerX * zoomLevel;
|
||||
const panY = canvasHeight / 2 - centerY * zoomLevel;
|
||||
|
||||
return { panX, panY };
|
||||
}
|
||||
|
||||
/**
|
||||
* 约束缩放级别
|
||||
*/
|
||||
export function constrainZoom(zoom: number): number {
|
||||
return Math.max(0.3, Math.min(zoom, 3));
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算缩放后的坐标
|
||||
*/
|
||||
export function transformCoordinate(
|
||||
x: number,
|
||||
y: number,
|
||||
panX: number,
|
||||
panY: number,
|
||||
zoomLevel: number
|
||||
): { x: number; y: number } {
|
||||
return {
|
||||
x: x * zoomLevel + panX,
|
||||
y: y * zoomLevel + panY
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算逆向变换的坐标(从屏幕坐标到画布坐标)
|
||||
*/
|
||||
export function inverseTransformCoordinate(
|
||||
screenX: number,
|
||||
screenY: number,
|
||||
panX: number,
|
||||
panY: number,
|
||||
zoomLevel: number
|
||||
): { x: number; y: number } {
|
||||
return {
|
||||
x: (screenX - panX) / zoomLevel,
|
||||
y: (screenY - panY) / zoomLevel
|
||||
};
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { InstallStatus } from '../types';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 检查行为树AI系统是否已安装
|
||||
* 通过主进程检查项目中是否安装了@esengine/ai包
|
||||
*/
|
||||
export async function checkBehaviorTreeInstalled(projectPath: string): Promise<InstallStatus> {
|
||||
try {
|
||||
// 通过Editor.Message请求主进程检查安装状态
|
||||
const isInstalled = await Editor.Message.request('cocos-ecs-extension', 'check-behavior-tree-installed');
|
||||
|
||||
if (isInstalled) {
|
||||
// 如果已安装,读取版本信息
|
||||
const packageJsonPath = path.join(projectPath, 'package.json');
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
||||
const aiPackage = dependencies['@esengine/ai'];
|
||||
|
||||
return {
|
||||
installed: true,
|
||||
version: aiPackage || null,
|
||||
packageExists: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
installed: false,
|
||||
version: null,
|
||||
packageExists: fs.existsSync(path.join(projectPath, 'package.json'))
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
installed: false,
|
||||
version: null,
|
||||
packageExists: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化安装状态文本
|
||||
*/
|
||||
export function getInstallStatusText(
|
||||
isChecking: boolean,
|
||||
isInstalling: boolean,
|
||||
isInstalled: boolean,
|
||||
version: string | null
|
||||
): string {
|
||||
if (isChecking) return '检查状态中...';
|
||||
if (isInstalling) return '正在安装AI系统...';
|
||||
return isInstalled ? 'AI系统已安装' : 'AI系统未安装';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取安装状态CSS类
|
||||
*/
|
||||
export function getInstallStatusClass(
|
||||
isInstalling: boolean,
|
||||
isInstalled: boolean
|
||||
): string {
|
||||
if (isInstalling) return 'installing';
|
||||
return isInstalled ? 'installed' : 'not-installed';
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装行为树AI系统
|
||||
* 通过发送消息到主进程来执行真实的npm安装命令
|
||||
*/
|
||||
export async function installBehaviorTreeAI(projectPath: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await Editor.Message.request('cocos-ecs-extension', 'install-behavior-tree');
|
||||
return Boolean(result);
|
||||
} catch (error) {
|
||||
console.error('请求安装AI系统失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新行为树AI系统
|
||||
* 通过发送消息到主进程来执行真实的npm更新命令
|
||||
*/
|
||||
export async function updateBehaviorTreeAI(projectPath: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await Editor.Message.request('cocos-ecs-extension', 'update-behavior-tree');
|
||||
return Boolean(result);
|
||||
} catch (error) {
|
||||
console.error('请求更新AI系统失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
import { TreeNode, ValidationResult } from '../types';
|
||||
import { NodeTemplate } from '../data/nodeTemplates';
|
||||
|
||||
/**
|
||||
* 生成唯一的节点ID
|
||||
*/
|
||||
export function generateNodeId(): string {
|
||||
return 'node_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模板创建节点
|
||||
*/
|
||||
export function createNodeFromTemplate(template: NodeTemplate, x: number = 100, y: number = 100): TreeNode {
|
||||
const nodeId = generateNodeId();
|
||||
|
||||
// 深拷贝 properties 以避免引用共享
|
||||
let properties: any = {};
|
||||
if (template.properties) {
|
||||
for (const [key, prop] of Object.entries(template.properties)) {
|
||||
properties[key] = {
|
||||
name: prop.name,
|
||||
type: prop.type,
|
||||
value: prop.value,
|
||||
description: prop.description,
|
||||
options: prop.options ? [...prop.options] : undefined,
|
||||
required: prop.required
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const node: TreeNode = {
|
||||
id: nodeId,
|
||||
type: template.type,
|
||||
name: template.name,
|
||||
icon: template.icon,
|
||||
description: template.description,
|
||||
x: x,
|
||||
y: y,
|
||||
children: [],
|
||||
properties: properties,
|
||||
canHaveChildren: template.canHaveChildren,
|
||||
canHaveParent: template.canHaveParent,
|
||||
maxChildren: template.maxChildren,
|
||||
minChildren: template.minChildren,
|
||||
hasError: false
|
||||
};
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找节点
|
||||
*/
|
||||
export function getNodeById(nodes: TreeNode[], id: string): TreeNode | undefined {
|
||||
return nodes.find(node => node.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取根节点
|
||||
*/
|
||||
export function getRootNode(nodes: TreeNode[]): TreeNode | undefined {
|
||||
return nodes.find(node => !node.parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归删除节点及其子节点
|
||||
*/
|
||||
export function deleteNodeRecursive(
|
||||
nodes: TreeNode[],
|
||||
nodeId: string,
|
||||
connections: any[],
|
||||
onConnectionsUpdate: (connections: any[]) => void
|
||||
): TreeNode[] {
|
||||
const deleteRecursive = (id: string) => {
|
||||
const node = getNodeById(nodes, id);
|
||||
if (!node) return;
|
||||
|
||||
// 递归删除子节点
|
||||
node.children.forEach(childId => deleteRecursive(childId));
|
||||
|
||||
// 从父节点的children中移除
|
||||
if (node.parent) {
|
||||
const parent = getNodeById(nodes, node.parent);
|
||||
if (parent) {
|
||||
const index = parent.children.indexOf(id);
|
||||
if (index > -1) {
|
||||
parent.children.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移除连接
|
||||
const updatedConnections = connections.filter(conn =>
|
||||
conn.sourceId !== id && conn.targetId !== id
|
||||
);
|
||||
onConnectionsUpdate(updatedConnections);
|
||||
|
||||
// 从树中移除节点
|
||||
const nodeIndex = nodes.findIndex(n => n.id === id);
|
||||
if (nodeIndex > -1) {
|
||||
nodes.splice(nodeIndex, 1);
|
||||
}
|
||||
};
|
||||
|
||||
deleteRecursive(nodeId);
|
||||
return [...nodes]; // 返回新数组以触发响应式更新
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证行为树结构
|
||||
*/
|
||||
export function validateTree(nodes: TreeNode[]): ValidationResult {
|
||||
if (nodes.length === 0) {
|
||||
return { isValid: false, message: '行为树为空' };
|
||||
}
|
||||
|
||||
// 检查根节点
|
||||
const rootNodes = nodes.filter(node => !node.parent);
|
||||
if (rootNodes.length === 0) {
|
||||
return { isValid: false, message: '缺少根节点' };
|
||||
}
|
||||
if (rootNodes.length > 1) {
|
||||
return { isValid: false, message: `发现多个根节点: ${rootNodes.map(n => n.name).join(', ')}` };
|
||||
}
|
||||
|
||||
// 验证每个节点的子节点数量限制
|
||||
for (const node of nodes) {
|
||||
const childrenCount = node.children.length;
|
||||
|
||||
// 检查最小子节点数量
|
||||
if (node.minChildren !== undefined && childrenCount < node.minChildren) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `节点 "${node.name}" 需要至少 ${node.minChildren} 个子节点,当前只有 ${childrenCount} 个`
|
||||
};
|
||||
}
|
||||
|
||||
// 检查最大子节点数量
|
||||
if (node.maxChildren !== undefined && childrenCount > node.maxChildren) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `节点 "${node.name}" 最多只能有 ${node.maxChildren} 个子节点,当前有 ${childrenCount} 个`
|
||||
};
|
||||
}
|
||||
|
||||
// 检查装饰器节点的特殊限制
|
||||
if (node.type.includes('decorator') || node.type.includes('Decorator')) {
|
||||
if (childrenCount !== 1) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `装饰器节点 "${node.name}" 必须有且只能有一个子节点,当前有 ${childrenCount} 个`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 检查叶子节点不能有子节点
|
||||
if (!node.canHaveChildren && childrenCount > 0) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: `叶子节点 "${node.name}" 不能有子节点,但当前有 ${childrenCount} 个`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 检查循环引用
|
||||
const visited = new Set<string>();
|
||||
const recursionStack = new Set<string>();
|
||||
|
||||
function hasCycle(nodeId: string): boolean {
|
||||
if (recursionStack.has(nodeId)) return true;
|
||||
if (visited.has(nodeId)) return false;
|
||||
|
||||
visited.add(nodeId);
|
||||
recursionStack.add(nodeId);
|
||||
|
||||
const node = getNodeById(nodes, nodeId);
|
||||
if (node) {
|
||||
for (const childId of node.children) {
|
||||
if (hasCycle(childId)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
recursionStack.delete(nodeId);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const node of nodes) {
|
||||
if (hasCycle(node.id)) {
|
||||
return { isValid: false, message: '检测到循环引用' };
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true, message: '行为树结构有效' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新节点属性
|
||||
*/
|
||||
export function updateNodeProperty(node: TreeNode, path: string, value: any): void {
|
||||
if (!node.properties) return;
|
||||
|
||||
const keys = path.split('.');
|
||||
let target: any = node.properties;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
target = target[keys[i]];
|
||||
}
|
||||
|
||||
target[keys[keys.length - 1]] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算节点的边界框
|
||||
*/
|
||||
export function getNodesBounds(nodes: TreeNode[]): { minX: number; minY: number; maxX: number; maxY: number } {
|
||||
if (nodes.length === 0) {
|
||||
return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
||||
}
|
||||
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
|
||||
nodes.forEach(node => {
|
||||
minX = Math.min(minX, node.x);
|
||||
minY = Math.min(minY, node.y);
|
||||
maxX = Math.max(maxX, node.x + 150); // 节点宽度
|
||||
maxY = Math.max(maxY, node.y + 100); // 节点高度
|
||||
});
|
||||
|
||||
return { minX, minY, maxX, maxY };
|
||||
}
|
||||
@@ -1,717 +0,0 @@
|
||||
import { readFileSync } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { createApp, App, defineComponent, ref, reactive, onMounted, onUnmounted } from 'vue';
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { IncomingMessage } from 'http';
|
||||
|
||||
const panelDataMap = new WeakMap<any, App>();
|
||||
|
||||
/**
|
||||
* 游戏实例信息
|
||||
*/
|
||||
interface GameInstance {
|
||||
id: string;
|
||||
name: string;
|
||||
connectTime: number;
|
||||
lastUpdateTime: number;
|
||||
isActive: boolean;
|
||||
debugData?: any;
|
||||
ws?: WebSocket; // WebSocket连接
|
||||
}
|
||||
|
||||
/**
|
||||
* 详细的调试信息接口
|
||||
*/
|
||||
interface DetailedDebugInfo {
|
||||
// 基础信息
|
||||
instanceId: string;
|
||||
instanceName: string;
|
||||
isRunning: boolean;
|
||||
frameworkLoaded: boolean;
|
||||
currentScene: string;
|
||||
uptime: number;
|
||||
|
||||
// 性能信息
|
||||
performance: {
|
||||
frameTime: number;
|
||||
fps: number;
|
||||
averageFrameTime: number;
|
||||
minFrameTime: number;
|
||||
maxFrameTime: number;
|
||||
frameTimeHistory: number[];
|
||||
engineFrameTime: number;
|
||||
ecsPercentage: number;
|
||||
};
|
||||
|
||||
// 内存信息
|
||||
memory: {
|
||||
totalMemory: number;
|
||||
usedMemory: number;
|
||||
freeMemory: number;
|
||||
entityMemory: number;
|
||||
componentMemory: number;
|
||||
systemMemory: number;
|
||||
pooledMemory: number;
|
||||
gcCollections: number;
|
||||
};
|
||||
|
||||
// 实体信息
|
||||
entities: {
|
||||
total: number;
|
||||
active: number;
|
||||
inactive: number;
|
||||
pendingAdd: number;
|
||||
pendingRemove: number;
|
||||
entitiesPerArchetype: Array<{
|
||||
signature: string;
|
||||
count: number;
|
||||
memory: number;
|
||||
}>;
|
||||
topEntitiesByComponents: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
componentCount: number;
|
||||
memory: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
// 组件信息
|
||||
components: {
|
||||
totalTypes: number;
|
||||
totalInstances: number;
|
||||
componentStats: Array<{
|
||||
typeName: string;
|
||||
instanceCount: number;
|
||||
memoryPerInstance: number;
|
||||
totalMemory: number;
|
||||
poolSize: number;
|
||||
poolUtilization: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
// 系统信息
|
||||
systems: {
|
||||
total: number;
|
||||
systemStats: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
entityCount: number;
|
||||
averageExecutionTime: number;
|
||||
minExecutionTime: number;
|
||||
maxExecutionTime: number;
|
||||
executionTimeHistory: number[];
|
||||
memoryUsage: number;
|
||||
updateOrder: number;
|
||||
enabled: boolean;
|
||||
percentage: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
// 场景信息
|
||||
scenes: {
|
||||
currentScene: string;
|
||||
sceneMemory: number;
|
||||
sceneEntityCount: number;
|
||||
sceneSystemCount: number;
|
||||
sceneUptime: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* ECS调试服务器
|
||||
* 作为服务端,接收多个游戏实例的连接
|
||||
*/
|
||||
class ECSDebugServer {
|
||||
private wss?: WebSocketServer;
|
||||
private port: number = 8080;
|
||||
private gameInstances = new Map<string, GameInstance>();
|
||||
private isRunning: boolean = false;
|
||||
|
||||
constructor(port: number = 8080) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
async start(): Promise<boolean> {
|
||||
if (this.isRunning) return true;
|
||||
|
||||
try {
|
||||
this.wss = new WebSocketServer({ port: this.port });
|
||||
|
||||
this.wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {
|
||||
const instanceId = this.generateInstanceId();
|
||||
const instance: GameInstance = {
|
||||
id: instanceId,
|
||||
name: `游戏实例-${instanceId.substring(0, 8)}`,
|
||||
connectTime: Date.now(),
|
||||
lastUpdateTime: Date.now(),
|
||||
isActive: true,
|
||||
debugData: null,
|
||||
ws: ws
|
||||
};
|
||||
|
||||
this.gameInstances.set(instanceId, instance);
|
||||
console.log(`[ECS Debug Server] New instance connected: ${instance.name}`);
|
||||
|
||||
ws.on('message', (data: Buffer) => {
|
||||
try {
|
||||
const message = JSON.parse(data.toString());
|
||||
this.handleMessage(instanceId, message);
|
||||
} catch (error) {
|
||||
console.error('[ECS Debug Server] Failed to parse message:', error);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
const instance = this.gameInstances.get(instanceId);
|
||||
if (instance) {
|
||||
instance.isActive = false;
|
||||
console.log(`[ECS Debug Server] Instance disconnected: ${instance.name}`);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', (error: Error) => {
|
||||
console.error(`[ECS Debug Server] WebSocket error for ${instanceId}:`, error);
|
||||
});
|
||||
|
||||
// 发送连接确认
|
||||
this.sendToInstance(instanceId, {
|
||||
type: 'connection_confirmed',
|
||||
instanceId: instanceId,
|
||||
serverTime: Date.now()
|
||||
});
|
||||
});
|
||||
|
||||
this.isRunning = true;
|
||||
console.log(`[ECS Debug Server] Started on port ${this.port}`);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ECS Debug Server] Failed to start:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.wss) {
|
||||
this.wss.close();
|
||||
this.wss = undefined;
|
||||
}
|
||||
this.gameInstances.clear();
|
||||
this.isRunning = false;
|
||||
console.log('[ECS Debug Server] Stopped');
|
||||
}
|
||||
|
||||
private generateInstanceId(): string {
|
||||
return Math.random().toString(36).substring(2) + Date.now().toString(36);
|
||||
}
|
||||
|
||||
private handleMessage(instanceId: string, message: any): void {
|
||||
const instance = this.gameInstances.get(instanceId);
|
||||
if (!instance) return;
|
||||
|
||||
switch (message.type) {
|
||||
case 'debug_data':
|
||||
instance.debugData = message.data;
|
||||
instance.lastUpdateTime = Date.now();
|
||||
break;
|
||||
|
||||
case 'instance_info':
|
||||
if (message.name) {
|
||||
instance.name = message.name;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ping':
|
||||
this.sendToInstance(instanceId, { type: 'pong', timestamp: Date.now() });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private sendToInstance(instanceId: string, message: any): void {
|
||||
const instance = this.gameInstances.get(instanceId);
|
||||
if (instance && instance.ws && instance.ws.readyState === 1) {
|
||||
instance.ws.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
get running(): boolean {
|
||||
return this.isRunning;
|
||||
}
|
||||
|
||||
get instances(): GameInstance[] {
|
||||
return Array.from(this.gameInstances.values());
|
||||
}
|
||||
|
||||
getInstance(instanceId: string): GameInstance | undefined {
|
||||
return this.gameInstances.get(instanceId);
|
||||
}
|
||||
|
||||
getInstanceDebugData(instanceId: string): DetailedDebugInfo | null {
|
||||
const instance = this.gameInstances.get(instanceId);
|
||||
if (!instance || !instance.debugData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.transformToDetailedDebugInfo(instance, instance.debugData);
|
||||
}
|
||||
|
||||
private transformToDetailedDebugInfo(instance: GameInstance, rawData: any): DetailedDebugInfo {
|
||||
const uptime = (Date.now() - instance.connectTime) / 1000;
|
||||
|
||||
// 计算系统性能数据,包括ECS占比
|
||||
const systemBreakdown = rawData.performance?.systemBreakdown || [];
|
||||
const systemPerformance = rawData.performance?.systemPerformance || [];
|
||||
|
||||
// 创建系统名称到占比的映射
|
||||
const systemPercentageMap = new Map<string, number>();
|
||||
systemBreakdown.forEach((sys: any) => {
|
||||
systemPercentageMap.set(sys.systemName, sys.percentage || 0);
|
||||
});
|
||||
|
||||
return {
|
||||
instanceId: instance.id,
|
||||
instanceName: instance.name,
|
||||
isRunning: rawData.isRunning || false,
|
||||
frameworkLoaded: rawData.frameworkLoaded || false,
|
||||
currentScene: rawData.currentScene || '未知',
|
||||
uptime: uptime,
|
||||
|
||||
performance: {
|
||||
frameTime: rawData.performance?.frameTime || 0,
|
||||
fps: rawData.performance?.fps || 0,
|
||||
averageFrameTime: rawData.performance?.averageFrameTime || rawData.performance?.frameTime || 0,
|
||||
minFrameTime: rawData.performance?.minFrameTime || rawData.performance?.frameTime || 0,
|
||||
maxFrameTime: rawData.performance?.maxFrameTime || rawData.performance?.frameTime || 0,
|
||||
frameTimeHistory: rawData.performance?.frameTimeHistory || [],
|
||||
engineFrameTime: rawData.performance?.engineFrameTime || 0,
|
||||
ecsPercentage: rawData.performance?.ecsPercentage || 0
|
||||
},
|
||||
|
||||
memory: {
|
||||
totalMemory: rawData.performance?.memoryDetails?.totalMemory || rawData.memory?.totalMemory || 512 * 1024 * 1024,
|
||||
usedMemory: rawData.performance?.memoryDetails?.usedMemory || (rawData.performance?.memoryUsage ? rawData.performance.memoryUsage * 1024 * 1024 : 0),
|
||||
freeMemory: rawData.performance?.memoryDetails?.freeMemory || 0,
|
||||
entityMemory: rawData.performance?.memoryDetails?.entities || rawData.memory?.entityMemory || 0,
|
||||
componentMemory: rawData.performance?.memoryDetails?.components || rawData.memory?.componentMemory || 0,
|
||||
systemMemory: rawData.performance?.memoryDetails?.systems || rawData.memory?.systemMemory || 0,
|
||||
pooledMemory: rawData.performance?.memoryDetails?.pooled || rawData.memory?.pooledMemory || 0,
|
||||
gcCollections: rawData.performance?.memoryDetails?.gcCollections || rawData.memory?.gcCollections || 0
|
||||
},
|
||||
|
||||
entities: {
|
||||
total: rawData.entities?.totalEntities || 0,
|
||||
active: rawData.entities?.activeEntities || 0,
|
||||
inactive: (rawData.entities?.totalEntities || 0) - (rawData.entities?.activeEntities || 0),
|
||||
pendingAdd: rawData.entities?.pendingAdd || 0,
|
||||
pendingRemove: rawData.entities?.pendingRemove || 0,
|
||||
entitiesPerArchetype: rawData.entities?.entitiesPerArchetype || [],
|
||||
topEntitiesByComponents: rawData.entities?.topEntitiesByComponents || []
|
||||
},
|
||||
|
||||
components: {
|
||||
totalTypes: rawData.components?.componentTypes || 0,
|
||||
totalInstances: rawData.components?.componentInstances || 0,
|
||||
componentStats: (rawData.components?.componentStats || []).map((comp: any) => ({
|
||||
typeName: comp.typeName,
|
||||
instanceCount: comp.instanceCount || 0,
|
||||
memoryPerInstance: comp.memoryPerInstance || 0,
|
||||
totalMemory: comp.totalMemory || (comp.instanceCount || 0) * (comp.memoryPerInstance || 0),
|
||||
poolSize: comp.poolSize || 0,
|
||||
poolUtilization: comp.poolSize > 0 ? (comp.instanceCount / comp.poolSize * 100) : 0
|
||||
}))
|
||||
},
|
||||
|
||||
systems: {
|
||||
total: rawData.systems?.totalSystems || 0,
|
||||
systemStats: (rawData.systems?.systemsInfo || []).map((sys: any) => {
|
||||
const systemName = sys.name;
|
||||
const percentage = systemPercentageMap.get(systemName) || 0;
|
||||
|
||||
return {
|
||||
name: systemName,
|
||||
type: sys.type || 'Unknown',
|
||||
entityCount: sys.entityCount || 0,
|
||||
averageExecutionTime: sys.executionTime || 0,
|
||||
minExecutionTime: sys.minExecutionTime || sys.executionTime || 0,
|
||||
maxExecutionTime: sys.maxExecutionTime || sys.executionTime || 0,
|
||||
executionTimeHistory: sys.executionTimeHistory || [],
|
||||
memoryUsage: sys.memoryUsage || 0,
|
||||
updateOrder: sys.updateOrder || 0,
|
||||
enabled: sys.enabled !== false,
|
||||
percentage: percentage
|
||||
};
|
||||
})
|
||||
},
|
||||
|
||||
scenes: {
|
||||
currentScene: rawData.currentScene || '未知',
|
||||
sceneMemory: rawData.scenes?.sceneMemory || 0,
|
||||
sceneEntityCount: rawData.entities?.totalEntities || 0,
|
||||
sceneSystemCount: rawData.systems?.totalSystems || 0,
|
||||
sceneUptime: rawData.scenes?.sceneUptime || uptime
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认调试信息
|
||||
*/
|
||||
const defaultDebugInfo: DetailedDebugInfo = {
|
||||
instanceId: '',
|
||||
instanceName: '未选择实例',
|
||||
isRunning: false,
|
||||
frameworkLoaded: false,
|
||||
currentScene: '未知',
|
||||
uptime: 0,
|
||||
performance: {
|
||||
frameTime: 0,
|
||||
fps: 0,
|
||||
averageFrameTime: 0,
|
||||
minFrameTime: 0,
|
||||
maxFrameTime: 0,
|
||||
frameTimeHistory: [],
|
||||
engineFrameTime: 0,
|
||||
ecsPercentage: 0
|
||||
},
|
||||
memory: {
|
||||
totalMemory: 0,
|
||||
usedMemory: 0,
|
||||
freeMemory: 0,
|
||||
entityMemory: 0,
|
||||
componentMemory: 0,
|
||||
systemMemory: 0,
|
||||
pooledMemory: 0,
|
||||
gcCollections: 0
|
||||
},
|
||||
entities: {
|
||||
total: 0,
|
||||
active: 0,
|
||||
inactive: 0,
|
||||
pendingAdd: 0,
|
||||
pendingRemove: 0,
|
||||
entitiesPerArchetype: [],
|
||||
topEntitiesByComponents: []
|
||||
},
|
||||
components: {
|
||||
totalTypes: 0,
|
||||
totalInstances: 0,
|
||||
componentStats: []
|
||||
},
|
||||
systems: {
|
||||
total: 0,
|
||||
systemStats: []
|
||||
},
|
||||
scenes: {
|
||||
currentScene: '未知',
|
||||
sceneMemory: 0,
|
||||
sceneEntityCount: 0,
|
||||
sceneSystemCount: 0,
|
||||
sceneUptime: 0
|
||||
}
|
||||
};
|
||||
|
||||
// 全局调试服务器实例
|
||||
let globalDebugServer: ECSDebugServer | null = null;
|
||||
|
||||
/**
|
||||
* 启动调试服务器
|
||||
*/
|
||||
async function ensureDebugServer(): Promise<ECSDebugServer> {
|
||||
if (!globalDebugServer) {
|
||||
globalDebugServer = new ECSDebugServer(8080);
|
||||
}
|
||||
|
||||
if (!globalDebugServer.running) {
|
||||
await globalDebugServer.start();
|
||||
}
|
||||
|
||||
return globalDebugServer;
|
||||
}
|
||||
|
||||
module.exports = Editor.Panel.define({
|
||||
listeners: {
|
||||
show() { },
|
||||
hide() { },
|
||||
},
|
||||
template: `<div id="app"></div>`,
|
||||
style: readFileSync(join(__dirname, '../../../static/style/debug/index.css'), 'utf-8'),
|
||||
$: {
|
||||
app: '#app',
|
||||
},
|
||||
ready() {
|
||||
if (this.$.app) {
|
||||
const app = createApp(defineComponent({
|
||||
setup() {
|
||||
const debugInfo = reactive<DetailedDebugInfo>({ ...defaultDebugInfo });
|
||||
const gameInstances = ref<GameInstance[]>([]);
|
||||
const selectedInstanceId = ref<string>('');
|
||||
const isAutoRefresh = ref(true);
|
||||
const refreshInterval = ref(100);
|
||||
const lastUpdateTime = ref('');
|
||||
const showComponentPoolHelp = ref(false);
|
||||
|
||||
let intervalId: NodeJS.Timeout | null = null;
|
||||
let debugServer: ECSDebugServer | null = null;
|
||||
|
||||
// 初始化调试服务器
|
||||
const initializeServer = async () => {
|
||||
try {
|
||||
debugServer = await ensureDebugServer();
|
||||
} catch (error) {
|
||||
console.error('Failed to start debug server:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 更新游戏实例列表和调试信息
|
||||
const updateDebugInfo = async () => {
|
||||
if (!debugServer) return;
|
||||
|
||||
try {
|
||||
// 更新实例列表
|
||||
gameInstances.value = debugServer.instances;
|
||||
|
||||
// 如果有选中的实例,更新其调试信息
|
||||
if (selectedInstanceId.value) {
|
||||
const detailedInfo = debugServer.getInstanceDebugData(selectedInstanceId.value);
|
||||
if (detailedInfo) {
|
||||
Object.assign(debugInfo, detailedInfo);
|
||||
} else {
|
||||
// 实例已断开,重置选择
|
||||
selectedInstanceId.value = '';
|
||||
Object.assign(debugInfo, defaultDebugInfo);
|
||||
}
|
||||
}
|
||||
|
||||
lastUpdateTime.value = new Date().toLocaleTimeString();
|
||||
} catch (error) {
|
||||
console.error('Failed to update debug info:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始自动刷新
|
||||
const startAutoRefresh = () => {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
|
||||
if (isAutoRefresh.value) {
|
||||
intervalId = setInterval(updateDebugInfo, refreshInterval.value);
|
||||
}
|
||||
};
|
||||
|
||||
// 停止自动刷新
|
||||
const stopAutoRefresh = () => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 手动刷新
|
||||
const manualRefresh = () => {
|
||||
updateDebugInfo();
|
||||
};
|
||||
|
||||
// 切换自动刷新
|
||||
const toggleAutoRefresh = () => {
|
||||
if (isAutoRefresh.value) {
|
||||
startAutoRefresh();
|
||||
} else {
|
||||
stopAutoRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
// 更改刷新间隔
|
||||
const changeRefreshInterval = () => {
|
||||
if (isAutoRefresh.value) {
|
||||
startAutoRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
// 实例选择改变
|
||||
const onInstanceChanged = () => {
|
||||
if (selectedInstanceId.value) {
|
||||
updateDebugInfo();
|
||||
} else {
|
||||
Object.assign(debugInfo, defaultDebugInfo);
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化运行时间
|
||||
const formatUptime = (seconds: number): string => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// 格式化内存大小
|
||||
const formatMemory = (bytes: number): string => {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
|
||||
};
|
||||
|
||||
// 获取FPS颜色
|
||||
const getFpsColor = (fps: number): string => {
|
||||
if (fps >= 55) return 'good';
|
||||
if (fps >= 30) return 'warning';
|
||||
return 'critical';
|
||||
};
|
||||
|
||||
// 获取内存颜色
|
||||
const getMemoryColor = (percentage: number): string => {
|
||||
if (percentage < 70) return 'good';
|
||||
if (percentage < 85) return 'warning';
|
||||
return 'critical';
|
||||
};
|
||||
|
||||
// 获取ECS时间占比颜色
|
||||
const getECSTimeColor = (percentage: number): string => {
|
||||
if (!percentage) return 'good';
|
||||
if (percentage <= 10) return 'good'; // ECS占用<=10%为绿色
|
||||
if (percentage <= 30) return 'warning'; // ECS占用<=30%为黄色
|
||||
return 'critical'; // ECS占用>30%为红色
|
||||
};
|
||||
|
||||
// 获取执行时间颜色
|
||||
const getExecutionTimeColor = (time: number): string => {
|
||||
if (time < 1) return 'good'; // <1ms为绿色
|
||||
if (time < 5) return 'warning'; // <5ms为黄色
|
||||
return 'critical';
|
||||
};
|
||||
|
||||
// 打开文档链接
|
||||
const openDocumentation = (section: string): void => {
|
||||
const urls: Record<string, string> = {
|
||||
'component-pool': 'https://github.com/esengine/ecs-framework/tree/master/docs/component-design-guide.md#1-对象池优化',
|
||||
'performance-optimization': 'https://github.com/esengine/ecs-framework/tree/master/docs/performance-optimization.md'
|
||||
};
|
||||
|
||||
const url = urls[section];
|
||||
if (!url) return;
|
||||
|
||||
try {
|
||||
// 在Cocos Creator扩展环境中,直接使用Electron的shell模块
|
||||
const { shell } = require('electron');
|
||||
shell.openExternal(url);
|
||||
} catch (error) {
|
||||
console.error('无法打开链接:', error);
|
||||
// 如果失败,复制到剪贴板
|
||||
copyUrlToClipboard(url);
|
||||
}
|
||||
};
|
||||
|
||||
// 复制链接到剪贴板的辅助函数
|
||||
const copyUrlToClipboard = (url: string): void => {
|
||||
try {
|
||||
// 尝试使用现代的剪贴板API
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
console.log(`文档链接已复制到剪贴板: ${url}`);
|
||||
// 如果可能的话,显示用户友好的提示
|
||||
if (typeof alert !== 'undefined') {
|
||||
alert(`文档链接已复制到剪贴板,请在浏览器中粘贴访问:\n${url}`);
|
||||
}
|
||||
}).catch(() => {
|
||||
fallbackCopyText(url);
|
||||
});
|
||||
} else {
|
||||
fallbackCopyText(url);
|
||||
}
|
||||
} catch (error) {
|
||||
fallbackCopyText(url);
|
||||
}
|
||||
};
|
||||
|
||||
// 备用的复制文本方法
|
||||
const fallbackCopyText = (text: string): void => {
|
||||
try {
|
||||
// 创建临时的文本区域
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
textArea.style.top = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
const successful = document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
if (successful) {
|
||||
console.log(`文档链接已复制到剪贴板: ${text}`);
|
||||
if (typeof alert !== 'undefined') {
|
||||
alert(`文档链接已复制到剪贴板,请在浏览器中粘贴访问:\n${text}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`请手动复制文档链接: ${text}`);
|
||||
if (typeof alert !== 'undefined') {
|
||||
alert(`请手动复制文档链接:\n${text}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`请手动访问文档: ${text}`);
|
||||
if (typeof alert !== 'undefined') {
|
||||
alert(`请手动访问文档:\n${text}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(async () => {
|
||||
await initializeServer();
|
||||
updateDebugInfo();
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
stopAutoRefresh();
|
||||
});
|
||||
|
||||
return {
|
||||
debugInfo,
|
||||
gameInstances,
|
||||
selectedInstanceId,
|
||||
isAutoRefresh,
|
||||
refreshInterval,
|
||||
lastUpdateTime,
|
||||
showComponentPoolHelp,
|
||||
manualRefresh,
|
||||
toggleAutoRefresh,
|
||||
changeRefreshInterval,
|
||||
onInstanceChanged,
|
||||
formatUptime,
|
||||
formatMemory,
|
||||
getFpsColor,
|
||||
getMemoryColor,
|
||||
getECSTimeColor,
|
||||
getExecutionTimeColor,
|
||||
openDocumentation
|
||||
};
|
||||
},
|
||||
template: readFileSync(join(__dirname, '../../../static/template/debug/index.html'), 'utf-8'),
|
||||
}));
|
||||
|
||||
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('ui-');
|
||||
app.mount(this.$.app);
|
||||
panelDataMap.set(this, app);
|
||||
}
|
||||
},
|
||||
beforeClose() { },
|
||||
close() {
|
||||
const app = panelDataMap.get(this);
|
||||
if (app) {
|
||||
app.unmount();
|
||||
panelDataMap.delete(this);
|
||||
}
|
||||
|
||||
// 关闭调试服务器
|
||||
if (globalDebugServer) {
|
||||
globalDebugServer.stop();
|
||||
globalDebugServer = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,528 +0,0 @@
|
||||
/* eslint-disable vue/one-component-per-file */
|
||||
|
||||
import { readFileSync } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { createApp, App, defineComponent, ref, onMounted } from 'vue';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
const panelDataMap = new WeakMap<any, App>();
|
||||
|
||||
/**
|
||||
* 检测ECS框架安装状态的工具函数
|
||||
*/
|
||||
async function checkEcsFrameworkStatus(projectPath: string) {
|
||||
const packageJsonPath = path.join(projectPath, 'package.json');
|
||||
const nodeModulesPath = path.join(projectPath, 'node_modules', '@esengine', 'ecs-framework');
|
||||
|
||||
try {
|
||||
// 检查package.json是否存在
|
||||
const packageJsonExists = fs.existsSync(packageJsonPath);
|
||||
if (!packageJsonExists) {
|
||||
return {
|
||||
packageJsonExists: false,
|
||||
ecsInstalled: false,
|
||||
ecsVersion: null
|
||||
};
|
||||
}
|
||||
|
||||
// 读取package.json
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
|
||||
// 检查依赖中是否包含ECS框架
|
||||
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
||||
const ecsInDeps = dependencies['@esengine/ecs-framework'];
|
||||
|
||||
// 检查node_modules中是否实际安装了ECS框架
|
||||
const ecsActuallyInstalled = fs.existsSync(nodeModulesPath);
|
||||
|
||||
let ecsVersion = null;
|
||||
if (ecsActuallyInstalled) {
|
||||
try {
|
||||
const ecsPackageJson = JSON.parse(
|
||||
fs.readFileSync(path.join(nodeModulesPath, 'package.json'), 'utf-8')
|
||||
);
|
||||
ecsVersion = ecsPackageJson.version;
|
||||
} catch (e) {
|
||||
console.warn('Unable to read ECS framework version:', e);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
packageJsonExists: true,
|
||||
ecsInstalled: ecsActuallyInstalled && !!ecsInDeps,
|
||||
ecsVersion,
|
||||
declaredVersion: ecsInDeps
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error checking ECS framework status:', error);
|
||||
return {
|
||||
packageJsonExists: false,
|
||||
ecsInstalled: false,
|
||||
ecsVersion: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测ECS模板状态
|
||||
*/
|
||||
function checkEcsTemplateStatus(projectPath: string) {
|
||||
const ecsDir = path.join(projectPath, 'assets', 'scripts', 'ecs');
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(ecsDir)) {
|
||||
return {
|
||||
templateExists: false,
|
||||
existingFiles: []
|
||||
};
|
||||
}
|
||||
|
||||
// 扫描ECS目录中的文件
|
||||
const existingFiles: string[] = [];
|
||||
function scanDirectory(dirPath: string, relativePath: string = '') {
|
||||
if (!fs.existsSync(dirPath)) return;
|
||||
|
||||
const items = fs.readdirSync(dirPath);
|
||||
for (const item of items) {
|
||||
const fullPath = path.join(dirPath, item);
|
||||
const relativeFilePath = relativePath ? `${relativePath}/${item}` : item;
|
||||
|
||||
if (fs.statSync(fullPath).isDirectory()) {
|
||||
scanDirectory(fullPath, relativeFilePath);
|
||||
} else {
|
||||
existingFiles.push(relativeFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scanDirectory(ecsDir);
|
||||
|
||||
return {
|
||||
templateExists: existingFiles.length > 0,
|
||||
existingFiles
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error checking ECS template status:', error);
|
||||
return {
|
||||
templateExists: false,
|
||||
existingFiles: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取ECS框架的最新版本
|
||||
*/
|
||||
function getLatestEcsVersion(): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
exec('npm view @esengine/ecs-framework version', (error, stdout) => {
|
||||
if (error) {
|
||||
console.warn('Failed to get latest version:', error);
|
||||
resolve(null);
|
||||
} else {
|
||||
resolve(stdout.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Node.js版本
|
||||
*/
|
||||
function getNodeVersion(): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
exec('node --version', (error, stdout) => {
|
||||
if (error) {
|
||||
resolve('未知');
|
||||
} else {
|
||||
resolve(stdout.trim().replace('v', ''));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较版本号
|
||||
*/
|
||||
function compareVersions(current: string, latest: string): boolean {
|
||||
try {
|
||||
const currentParts = current.split('.').map(Number);
|
||||
const latestParts = latest.split('.').map(Number);
|
||||
|
||||
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|
||||
const currentPart = currentParts[i] || 0;
|
||||
const latestPart = latestParts[i] || 0;
|
||||
|
||||
if (latestPart > currentPart) {
|
||||
return true; // 有更新
|
||||
} else if (latestPart < currentPart) {
|
||||
return false; // 当前版本更新
|
||||
}
|
||||
}
|
||||
return false; // 版本相同
|
||||
} catch (error) {
|
||||
console.warn('Version comparison failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 如果希望兼容 3.3 之前的版本可以使用下方的代码
|
||||
* @en You can add the code below if you want compatibility with versions prior to 3.3
|
||||
*/
|
||||
// Editor.Panel.define = Editor.Panel.define || function(options: any) { return options }
|
||||
|
||||
module.exports = Editor.Panel.define({
|
||||
listeners: {
|
||||
show() { console.log('ECS Welcome Panel Show'); },
|
||||
hide() { console.log('ECS Welcome Panel Hide'); },
|
||||
},
|
||||
template: readFileSync(join(__dirname, '../../../static/template/default/index.html'), 'utf-8'),
|
||||
style: readFileSync(join(__dirname, '../../../static/style/default/index.css'), 'utf-8'),
|
||||
$: {
|
||||
app: '#app',
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 向主进程发送消息的方法
|
||||
*/
|
||||
sendToMain(message: string, ...args: any[]) {
|
||||
Editor.Message.send('cocos-ecs-extension', message, ...args);
|
||||
}
|
||||
},
|
||||
ready() {
|
||||
if (this.$.app) {
|
||||
const app = createApp({});
|
||||
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('ui-');
|
||||
|
||||
// ECS欢迎组件
|
||||
app.component('EcsWelcome', defineComponent({
|
||||
setup() {
|
||||
// 响应式状态
|
||||
const checkingStatus = ref(true);
|
||||
const ecsInstalled = ref(false);
|
||||
const ecsVersion = ref<string | null>(null);
|
||||
const latestVersion = ref<string | null>(null);
|
||||
const hasUpdate = ref(false);
|
||||
const packageJsonExists = ref(false);
|
||||
const nodeVersion = ref('检测中...');
|
||||
const pluginVersion = ref('1.0.0');
|
||||
const lastCheckTime = ref<string | null>(null);
|
||||
|
||||
// ECS模板状态
|
||||
const templateExists = ref(false);
|
||||
const existingFiles = ref<string[]>([]);
|
||||
|
||||
// 操作状态
|
||||
const installing = ref(false);
|
||||
const updating = ref(false);
|
||||
const uninstalling = ref(false);
|
||||
|
||||
// 操作状态显示
|
||||
const showOperationStatus = ref(false);
|
||||
const operationStatusType = ref('loading');
|
||||
const operationStatusMessage = ref('');
|
||||
const operationStatusDetails = ref('');
|
||||
|
||||
// 显示操作状态
|
||||
const setOperationStatus = (type: string, message: string, details?: string) => {
|
||||
showOperationStatus.value = true;
|
||||
operationStatusType.value = type;
|
||||
operationStatusMessage.value = message;
|
||||
operationStatusDetails.value = details || '';
|
||||
|
||||
// 自动隐藏成功和警告消息
|
||||
if (type === 'success' || type === 'warning') {
|
||||
setTimeout(() => {
|
||||
showOperationStatus.value = false;
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态图标
|
||||
const getStatusIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'loading': return '⏳';
|
||||
case 'success': return '✅';
|
||||
case 'error':
|
||||
case 'failed': return '❌';
|
||||
case 'warning': return '⚠️';
|
||||
default: return 'ℹ️';
|
||||
}
|
||||
};
|
||||
|
||||
// 监听来自主进程的消息 - 暂时注释掉,使用定时刷新
|
||||
const setupMessageListeners = () => {
|
||||
// TODO: 使用正确的消息监听API
|
||||
console.log('Message listeners setup - using polling instead');
|
||||
};
|
||||
|
||||
// 定时检查状态(用于检测操作完成)
|
||||
let statusCheckInterval: any = null;
|
||||
const startStatusPolling = () => {
|
||||
if (statusCheckInterval) clearInterval(statusCheckInterval);
|
||||
statusCheckInterval = setInterval(() => {
|
||||
if (installing.value || updating.value || uninstalling.value) {
|
||||
checkStatus();
|
||||
}
|
||||
}, 3000); // 每3秒检查一次
|
||||
};
|
||||
|
||||
const stopStatusPolling = () => {
|
||||
if (statusCheckInterval) {
|
||||
clearInterval(statusCheckInterval);
|
||||
statusCheckInterval = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 检测状态的方法
|
||||
const checkStatus = async () => {
|
||||
checkingStatus.value = true;
|
||||
|
||||
try {
|
||||
// 获取当前项目路径
|
||||
const projectPath = Editor.Project.path;
|
||||
|
||||
// 检测ECS框架状态
|
||||
const status = await checkEcsFrameworkStatus(projectPath);
|
||||
const prevInstalled = ecsInstalled.value;
|
||||
const prevVersion = ecsVersion.value;
|
||||
|
||||
packageJsonExists.value = status.packageJsonExists;
|
||||
ecsInstalled.value = status.ecsInstalled;
|
||||
ecsVersion.value = status.ecsVersion;
|
||||
|
||||
// 检测ECS模板状态
|
||||
const templateStatus = checkEcsTemplateStatus(projectPath);
|
||||
templateExists.value = templateStatus.templateExists;
|
||||
existingFiles.value = templateStatus.existingFiles;
|
||||
|
||||
// 检测操作完成
|
||||
if (installing.value) {
|
||||
if (status.ecsInstalled && !prevInstalled) {
|
||||
installing.value = false;
|
||||
setOperationStatus('success', 'ECS Framework 安装成功!');
|
||||
stopStatusPolling();
|
||||
} else if (!status.ecsInstalled) {
|
||||
// 可能还在安装中,继续等待
|
||||
}
|
||||
}
|
||||
|
||||
if (updating.value) {
|
||||
if (status.ecsVersion && status.ecsVersion !== prevVersion) {
|
||||
updating.value = false;
|
||||
setOperationStatus('success', `ECS Framework 更新成功到 v${status.ecsVersion}!`);
|
||||
stopStatusPolling();
|
||||
}
|
||||
}
|
||||
|
||||
if (uninstalling.value) {
|
||||
if (!status.ecsInstalled && prevInstalled) {
|
||||
uninstalling.value = false;
|
||||
setOperationStatus('success', 'ECS Framework 卸载成功!');
|
||||
stopStatusPolling();
|
||||
}
|
||||
}
|
||||
|
||||
// 获取Node.js版本
|
||||
nodeVersion.value = await getNodeVersion();
|
||||
|
||||
// 检查更新
|
||||
if (ecsInstalled.value && ecsVersion.value) {
|
||||
await checkForUpdates();
|
||||
}
|
||||
|
||||
// 更新检查时间
|
||||
lastCheckTime.value = new Date().toLocaleString();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Status check failed:', error);
|
||||
|
||||
// 如果检查失败,停止操作状态
|
||||
if (installing.value || updating.value || uninstalling.value) {
|
||||
installing.value = false;
|
||||
updating.value = false;
|
||||
uninstalling.value = false;
|
||||
setOperationStatus('error', '状态检查失败,请手动验证操作结果');
|
||||
stopStatusPolling();
|
||||
}
|
||||
} finally {
|
||||
checkingStatus.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查更新
|
||||
const checkForUpdates = async () => {
|
||||
if (!ecsInstalled.value || !ecsVersion.value) {
|
||||
setOperationStatus('warning', '请先安装 ECS Framework');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setOperationStatus('loading', '正在检查更新...');
|
||||
|
||||
const latest = await getLatestEcsVersion();
|
||||
if (latest) {
|
||||
latestVersion.value = latest;
|
||||
const needsUpdate = compareVersions(ecsVersion.value, latest);
|
||||
hasUpdate.value = needsUpdate;
|
||||
|
||||
if (needsUpdate) {
|
||||
setOperationStatus('success', `发现新版本:v${latest}(当前:v${ecsVersion.value})`);
|
||||
} else {
|
||||
setOperationStatus('success', `已是最新版本:v${ecsVersion.value}`);
|
||||
}
|
||||
} else {
|
||||
setOperationStatus('warning', '无法获取最新版本信息,请检查网络连接');
|
||||
}
|
||||
|
||||
// 更新检查时间
|
||||
lastCheckTime.value = new Date().toLocaleString();
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Failed to check updates:', error);
|
||||
setOperationStatus('error', '检查更新失败,请检查网络连接');
|
||||
}
|
||||
};
|
||||
|
||||
// 操作方法
|
||||
const installEcsFramework = () => {
|
||||
if (!packageJsonExists.value || installing.value) return;
|
||||
|
||||
Editor.Dialog.info('安装 ECS Framework', {
|
||||
detail: '即将安装@esengine/ecs-framework到当前项目...',
|
||||
buttons: ['确定', '取消'],
|
||||
default: 0,
|
||||
}).then((result) => {
|
||||
if (result.response === 0) {
|
||||
installing.value = true;
|
||||
setOperationStatus('loading', '正在安装 ECS Framework...');
|
||||
startStatusPolling();
|
||||
|
||||
// 发送安装命令到主进程
|
||||
Editor.Message.send('cocos-ecs-extension', 'install-ecs-framework');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateEcsFramework = () => {
|
||||
if (!hasUpdate.value || updating.value) return;
|
||||
|
||||
Editor.Dialog.info('更新 ECS Framework', {
|
||||
detail: `即将更新ECS框架从 v${ecsVersion.value} 到 v${latestVersion.value}`,
|
||||
buttons: ['确定', '取消'],
|
||||
default: 0,
|
||||
}).then((result) => {
|
||||
if (result.response === 0) {
|
||||
updating.value = true;
|
||||
setOperationStatus('loading', `正在更新 ECS Framework 到 v${latestVersion.value}...`);
|
||||
startStatusPolling();
|
||||
|
||||
Editor.Message.send('cocos-ecs-extension', 'update-ecs-framework', latestVersion.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const uninstallEcsFramework = () => {
|
||||
if (uninstalling.value) return;
|
||||
|
||||
Editor.Dialog.warn('卸载 ECS Framework', {
|
||||
detail: '确定要卸载ECS框架吗?这将删除项目中的ECS框架依赖。',
|
||||
buttons: ['确定卸载', '取消'],
|
||||
default: 1,
|
||||
}).then((result) => {
|
||||
if (result.response === 0) {
|
||||
uninstalling.value = true;
|
||||
setOperationStatus('loading', '正在卸载 ECS Framework...');
|
||||
startStatusPolling();
|
||||
|
||||
Editor.Message.send('cocos-ecs-extension', 'uninstall-ecs-framework');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const openDocumentation = () => {
|
||||
if (!ecsInstalled.value) return;
|
||||
Editor.Message.send('cocos-ecs-extension', 'open-documentation');
|
||||
};
|
||||
|
||||
const createEcsTemplate = () => {
|
||||
if (!ecsInstalled.value || templateExists.value) return;
|
||||
|
||||
Editor.Dialog.info('创建 ECS 模板', {
|
||||
detail: '即将创建基础的ECS项目结构和启动代码...',
|
||||
buttons: ['确定', '取消'],
|
||||
default: 0,
|
||||
}).then((result) => {
|
||||
if (result.response === 0) {
|
||||
Editor.Message.send('cocos-ecs-extension', 'create-ecs-template');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const openGithub = () => {
|
||||
Editor.Message.send('cocos-ecs-extension', 'open-github');
|
||||
};
|
||||
|
||||
const joinQQGroup = () => {
|
||||
Editor.Message.send('cocos-ecs-extension', 'open-qq-group');
|
||||
};
|
||||
|
||||
const openGenerator = () => {
|
||||
Editor.Message.send('cocos-ecs-extension', 'open-generator');
|
||||
};
|
||||
|
||||
// 组件挂载后检测状态
|
||||
onMounted(() => {
|
||||
setupMessageListeners();
|
||||
checkStatus();
|
||||
});
|
||||
|
||||
return {
|
||||
checkingStatus,
|
||||
ecsInstalled,
|
||||
ecsVersion,
|
||||
latestVersion,
|
||||
hasUpdate,
|
||||
packageJsonExists,
|
||||
nodeVersion,
|
||||
pluginVersion,
|
||||
lastCheckTime,
|
||||
templateExists,
|
||||
existingFiles,
|
||||
installing,
|
||||
updating,
|
||||
uninstalling,
|
||||
showOperationStatus,
|
||||
operationStatusType,
|
||||
operationStatusMessage,
|
||||
operationStatusDetails,
|
||||
getStatusIcon,
|
||||
installEcsFramework,
|
||||
updateEcsFramework,
|
||||
uninstallEcsFramework,
|
||||
checkForUpdates,
|
||||
openDocumentation,
|
||||
createEcsTemplate,
|
||||
openGithub,
|
||||
joinQQGroup,
|
||||
openGenerator
|
||||
};
|
||||
},
|
||||
template: readFileSync(join(__dirname, '../../../static/template/vue/welcome.html'), 'utf-8'),
|
||||
}));
|
||||
|
||||
app.mount(this.$.app);
|
||||
panelDataMap.set(this, app);
|
||||
}
|
||||
},
|
||||
beforeClose() { },
|
||||
close() {
|
||||
const app = panelDataMap.get(this);
|
||||
if (app) {
|
||||
app.unmount();
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,240 +0,0 @@
|
||||
import { readFileSync } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import * as path from 'path';
|
||||
import { createApp, App, defineComponent, ref, reactive } from 'vue';
|
||||
import { CodeGenerator } from '../../CodeGenerator';
|
||||
|
||||
const panelDataMap = new WeakMap<any, App>();
|
||||
|
||||
module.exports = Editor.Panel.define({
|
||||
listeners: {
|
||||
show() { },
|
||||
hide() { },
|
||||
},
|
||||
template: `<div id="app"></div>`,
|
||||
style: readFileSync(join(__dirname, '../../../static/style/generator/index.css'), 'utf-8'),
|
||||
$: {
|
||||
app: '#app',
|
||||
},
|
||||
ready() {
|
||||
if (this.$.app) {
|
||||
const app = createApp(defineComponent({
|
||||
setup() {
|
||||
const featureName = ref('');
|
||||
const options = reactive({
|
||||
generateComponent: true,
|
||||
generateSystem: false
|
||||
});
|
||||
|
||||
// 组件选项
|
||||
const componentOptions = reactive({
|
||||
includeComments: true,
|
||||
addProperties: []
|
||||
});
|
||||
|
||||
// 系统选项
|
||||
const systemOptions = reactive({
|
||||
systemType: 'EntitySystem' as 'EntitySystem' | 'ProcessingSystem' | 'IntervalSystem' | 'PassiveSystem',
|
||||
includeComments: true,
|
||||
requiredComponents: [],
|
||||
filterByComponent: true
|
||||
});
|
||||
|
||||
// 系统类型定义
|
||||
const systemTypes = [
|
||||
{
|
||||
value: 'EntitySystem',
|
||||
name: 'EntitySystem',
|
||||
icon: '🔄',
|
||||
description: '批量处理实体,适合需要遍历多个实体的逻辑',
|
||||
usage: '适用场景:移动系统、渲染系统、物理碰撞系统'
|
||||
},
|
||||
{
|
||||
value: 'ProcessingSystem',
|
||||
name: 'ProcessingSystem',
|
||||
icon: '⚡',
|
||||
description: '执行全局逻辑,不依赖特定实体',
|
||||
usage: '适用场景:输入处理、音效管理、场景切换'
|
||||
},
|
||||
{
|
||||
value: 'IntervalSystem',
|
||||
name: 'IntervalSystem',
|
||||
icon: '⏰',
|
||||
description: '按时间间隔执行,可控制执行频率',
|
||||
usage: '适用场景:AI决策、状态保存、定时清理'
|
||||
},
|
||||
{
|
||||
value: 'PassiveSystem',
|
||||
name: 'PassiveSystem',
|
||||
icon: '🎯',
|
||||
description: '被动响应,需要手动调用或事件触发',
|
||||
usage: '适用场景:技能释放、道具使用、特殊效果'
|
||||
}
|
||||
];
|
||||
|
||||
const isGenerating = ref(false);
|
||||
const previewCode = ref('');
|
||||
const showPreview = ref(false);
|
||||
|
||||
// 选择系统类型
|
||||
const selectSystemType = (type: string) => {
|
||||
systemOptions.systemType = type as any;
|
||||
updatePreview();
|
||||
};
|
||||
|
||||
// 生成代码
|
||||
const generateCode = async () => {
|
||||
if (!featureName.value.trim()) {
|
||||
Editor.Dialog.warn('请输入功能名称', {
|
||||
detail: '请先输入一个有效的功能名称,例如:Health、Movement、Combat等'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.generateComponent && !options.generateSystem) {
|
||||
Editor.Dialog.warn('请选择生成内容', {
|
||||
detail: '请至少选择一种要生成的代码类型(组件或系统)'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
isGenerating.value = true;
|
||||
|
||||
try {
|
||||
const projectPath = Editor.Project.path;
|
||||
const ecsDir = path.join(projectPath, 'assets', 'scripts', 'ecs');
|
||||
|
||||
// 检查ECS目录是否存在
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync(ecsDir)) {
|
||||
Editor.Dialog.warn('ECS目录不存在', {
|
||||
detail: '请先创建ECS模板后再生成代码。\n\n您可以在欢迎面板中点击"创建ECS模板"来创建基础结构。',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const codeGenerator = new CodeGenerator();
|
||||
const generatedFiles: string[] = [];
|
||||
const baseName = featureName.value.trim();
|
||||
|
||||
// 生成组件
|
||||
if (options.generateComponent) {
|
||||
const componentDir = path.join(ecsDir, 'components');
|
||||
await codeGenerator.generateComponent(baseName, componentDir, componentOptions);
|
||||
generatedFiles.push(`📦 组件: ${baseName}Component.ts`);
|
||||
}
|
||||
|
||||
// 生成系统
|
||||
if (options.generateSystem) {
|
||||
const systemDir = path.join(ecsDir, 'systems');
|
||||
// 如果选择了组件过滤且生成了组件,自动添加组件过滤
|
||||
const requiredComponents = (systemOptions.filterByComponent && options.generateComponent) ?
|
||||
[`${baseName}Component`] : [];
|
||||
|
||||
const systemOpts = {
|
||||
...systemOptions,
|
||||
requiredComponents
|
||||
};
|
||||
|
||||
await codeGenerator.generateSystem(
|
||||
baseName,
|
||||
systemDir,
|
||||
systemOpts
|
||||
);
|
||||
generatedFiles.push(`⚙️ 系统: ${baseName}System.ts`);
|
||||
}
|
||||
|
||||
// 成功提示
|
||||
Editor.Dialog.info('代码生成成功', {
|
||||
detail: `✅ ${baseName} 功能代码已生成完成!\n\n生成的文件:\n${generatedFiles.join('\n')}\n\n请刷新资源管理器查看新创建的文件。`
|
||||
});
|
||||
|
||||
// 清空输入
|
||||
featureName.value = '';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to generate code:', error);
|
||||
Editor.Dialog.error('代码生成失败', {
|
||||
detail: `生成代码时发生错误:\n\n${error}`
|
||||
});
|
||||
} finally {
|
||||
isGenerating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 预览代码
|
||||
const previewGeneration = () => {
|
||||
if (!featureName.value.trim()) {
|
||||
showPreview.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const baseName = featureName.value.trim();
|
||||
let preview = `将要生成的文件:\n\n`;
|
||||
|
||||
if (options.generateComponent) {
|
||||
preview += `📦 组件: ${baseName}Component.ts\n`;
|
||||
preview += ` - 位置: assets/scripts/ecs/components/\n`;
|
||||
preview += ` - 基础组件模板\n\n`;
|
||||
}
|
||||
|
||||
if (options.generateSystem) {
|
||||
const selectedType = systemTypes.find(t => t.value === systemOptions.systemType);
|
||||
preview += `⚙️ 系统: ${baseName}System.ts\n`;
|
||||
preview += ` - 位置: assets/scripts/ecs/systems/\n`;
|
||||
preview += ` - 类型: ${selectedType?.name || systemOptions.systemType}\n`;
|
||||
|
||||
if (systemOptions.filterByComponent && options.generateComponent) {
|
||||
preview += ` - 过滤组件: ${baseName}Component\n`;
|
||||
} else if (systemOptions.filterByComponent) {
|
||||
preview += ` - 组件过滤: 需要手动配置\n`;
|
||||
} else {
|
||||
preview += ` - 组件过滤: 无\n`;
|
||||
}
|
||||
preview += `\n`;
|
||||
}
|
||||
|
||||
previewCode.value = preview;
|
||||
showPreview.value = true;
|
||||
};
|
||||
|
||||
// 监听功能名称变化
|
||||
const updatePreview = () => {
|
||||
if (featureName.value.trim()) {
|
||||
previewGeneration();
|
||||
} else {
|
||||
showPreview.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
featureName,
|
||||
options,
|
||||
componentOptions,
|
||||
systemOptions,
|
||||
systemTypes,
|
||||
isGenerating,
|
||||
previewCode,
|
||||
showPreview,
|
||||
generateCode,
|
||||
updatePreview,
|
||||
selectSystemType
|
||||
};
|
||||
},
|
||||
template: readFileSync(join(__dirname, '../../../static/template/generator/index.html'), 'utf-8')
|
||||
}));
|
||||
|
||||
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('ui-');
|
||||
app.mount(this.$.app);
|
||||
panelDataMap.set(this, app);
|
||||
}
|
||||
},
|
||||
beforeClose() { },
|
||||
close() {
|
||||
const app = panelDataMap.get(this);
|
||||
if (app) {
|
||||
app.unmount();
|
||||
panelDataMap.delete(this);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,187 +0,0 @@
|
||||
import { join } from 'path';
|
||||
|
||||
// 添加编辑器内的模块搜索路径
|
||||
module.paths.push(join(Editor.App.path, 'node_modules'));
|
||||
|
||||
export function load() {
|
||||
console.log('ECS Debug Scene Script loaded');
|
||||
}
|
||||
|
||||
export function unload() {
|
||||
console.log('ECS Debug Scene Script unloaded');
|
||||
}
|
||||
|
||||
export const methods = {
|
||||
/**
|
||||
* 获取预览状态
|
||||
* @returns {object} 预览状态信息
|
||||
*/
|
||||
getPreviewState() {
|
||||
try {
|
||||
// 检查是否在游戏运行状态
|
||||
const { director } = require('cc');
|
||||
if (director && director.getScene && director.getScene()) {
|
||||
return {
|
||||
isRunning: true,
|
||||
engineLoaded: true
|
||||
};
|
||||
}
|
||||
return {
|
||||
isRunning: false,
|
||||
engineLoaded: false
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Failed to get preview state:', error);
|
||||
return {
|
||||
isRunning: false,
|
||||
engineLoaded: false
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查ECS框架是否已加载
|
||||
* @returns {boolean} ECS框架加载状态
|
||||
*/
|
||||
isECSFrameworkLoaded() {
|
||||
try {
|
||||
// 检查是否有ECS框架的全局对象
|
||||
return typeof window !== 'undefined' && !!(window as any).ECSFramework;
|
||||
} catch (error) {
|
||||
console.warn('Failed to check ECS framework status:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取场景基本信息
|
||||
* @returns {object} 场景信息
|
||||
*/
|
||||
getSceneBasicInfo() {
|
||||
try {
|
||||
const { director } = require('cc');
|
||||
if (director && director.getScene) {
|
||||
const scene = director.getScene();
|
||||
return {
|
||||
sceneName: scene ? (scene.name || '当前场景') : '未知场景',
|
||||
nodeCount: scene ? this.countNodes(scene) : 0,
|
||||
isValid: scene ? scene.isValid : false
|
||||
};
|
||||
}
|
||||
return {
|
||||
sceneName: '未知场景',
|
||||
nodeCount: 0,
|
||||
isValid: false
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Failed to get scene basic info:', error);
|
||||
return {
|
||||
sceneName: '获取失败',
|
||||
nodeCount: 0,
|
||||
isValid: false
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取ECS框架的调试信息
|
||||
* @returns {object|null} ECS调试数据或null(如果框架未加载)
|
||||
*/
|
||||
getECSDebugInfo() {
|
||||
try {
|
||||
// 检查是否有ECS框架的全局对象
|
||||
if (typeof window !== 'undefined' && (window as any).ECSFramework) {
|
||||
const ecs = (window as any).ECSFramework;
|
||||
|
||||
// 获取当前场景和实体管理器
|
||||
if (ecs.Core && ecs.Core.getCurrentScene) {
|
||||
const scene = ecs.Core.getCurrentScene();
|
||||
if (scene && scene.entityManager) {
|
||||
const entityManager = scene.entityManager;
|
||||
const systemManager = scene.systemManager;
|
||||
|
||||
// 收集调试信息
|
||||
const debugInfo = {
|
||||
timestamp: new Date().toISOString(),
|
||||
frameworkLoaded: true,
|
||||
currentScene: scene.name || '当前场景',
|
||||
totalEntities: entityManager.entityCount || 0,
|
||||
activeEntities: entityManager.activeEntityCount || 0,
|
||||
pendingAdd: 0, // 需要具体API
|
||||
pendingRemove: 0, // 需要具体API
|
||||
totalSystems: systemManager ? systemManager.getSystemCount() : 0,
|
||||
systemsInfo: [],
|
||||
frameTime: 0, // 需要性能监控
|
||||
memoryUsage: 0, // 需要内存监控
|
||||
componentTypes: 0, // 需要组件统计
|
||||
componentInstances: 0 // 需要组件实例统计
|
||||
};
|
||||
|
||||
// 获取系统信息
|
||||
if (systemManager && systemManager.getSystems) {
|
||||
const systems = systemManager.getSystems();
|
||||
debugInfo.systemsInfo = systems.map((system: any, index: number) => ({
|
||||
name: system.constructor.name || `System${index}`,
|
||||
entityCount: system.entities ? system.entities.length : 0,
|
||||
executionTime: system.lastExecutionTime || 0,
|
||||
updateOrder: index + 1
|
||||
}));
|
||||
}
|
||||
|
||||
return debugInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否直接导入了ECS模块
|
||||
try {
|
||||
// 这里需要根据实际的ECS框架导入方式调整
|
||||
const { Core } = require('ecs-framework');
|
||||
if (Core) {
|
||||
const scene = Core.getCurrentScene();
|
||||
if (scene) {
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
frameworkLoaded: true,
|
||||
currentScene: scene.name || '当前场景',
|
||||
totalEntities: scene.entityManager?.entityCount || 0,
|
||||
activeEntities: scene.entityManager?.activeEntityCount || 0,
|
||||
pendingAdd: 0,
|
||||
pendingRemove: 0,
|
||||
totalSystems: scene.systemManager?.getSystemCount() || 0,
|
||||
systemsInfo: [],
|
||||
frameTime: 0,
|
||||
memoryUsage: 0,
|
||||
componentTypes: 0,
|
||||
componentInstances: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// ECS框架未导入或未初始化
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.warn('Failed to get ECS debug info:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 递归计算节点数量
|
||||
* @param {any} node
|
||||
* @returns {number}
|
||||
*/
|
||||
countNodes(node: any): number {
|
||||
if (!node) return 0;
|
||||
|
||||
let count = 1; // 当前节点
|
||||
if (node.children) {
|
||||
for (const child of node.children) {
|
||||
count += this.countNodes(child);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
title Cocos ECS Extension - 热更新管理后台
|
||||
|
||||
echo.
|
||||
echo ======================================
|
||||
echo 🚀 Cocos ECS Extension 热更新管理后台
|
||||
echo ======================================
|
||||
echo.
|
||||
|
||||
:: 检查Node.js是否安装
|
||||
node --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo ❌ 错误: 未检测到Node.js,请先安装Node.js
|
||||
echo 下载地址: https://nodejs.org/
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: 获取Node.js版本
|
||||
for /f "tokens=1" %%i in ('node --version') do set NODE_VERSION=%%i
|
||||
echo ✅ Node.js版本: %NODE_VERSION%
|
||||
|
||||
:: 检查是否首次运行
|
||||
if not exist "admin-backend\node_modules" (
|
||||
echo.
|
||||
echo 📦 首次运行,正在安装依赖...
|
||||
cd admin-backend
|
||||
call npm install
|
||||
if %errorlevel% neq 0 (
|
||||
echo ❌ 依赖安装失败
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
cd ..
|
||||
echo ✅ 依赖安装完成
|
||||
)
|
||||
|
||||
:: 启动服务
|
||||
echo.
|
||||
echo 🚀 启动热更新管理后台...
|
||||
echo 📍 管理界面地址: http://localhost:3001
|
||||
echo.
|
||||
echo 💡 提示: 按 Ctrl+C 可停止服务
|
||||
echo.
|
||||
|
||||
cd admin-backend
|
||||
call npm run dev
|
||||
|
||||
pause
|
||||
@@ -1,261 +0,0 @@
|
||||
/* 基础样式和布局 */
|
||||
|
||||
/* 全局重置和通用样式 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1a202c;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #4a5568;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #667eea;
|
||||
}
|
||||
|
||||
/* 选择文本样式 */
|
||||
::selection {
|
||||
background: rgba(102, 126, 234, 0.3);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
#behavior-tree-app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: #1e1e1e;
|
||||
color: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 编辑器容器 */
|
||||
.editor-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 画布容器 */
|
||||
.canvas-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #2d3748;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1400px) {
|
||||
.properties-panel-container {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.blackboard-sidebar {
|
||||
width: 260px;
|
||||
right: 280px;
|
||||
}
|
||||
|
||||
.blackboard-sidebar.collapsed {
|
||||
right: 232px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.nodes-panel {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.properties-panel-container {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.blackboard-sidebar {
|
||||
width: 240px;
|
||||
right: 260px;
|
||||
}
|
||||
|
||||
.blackboard-sidebar.collapsed {
|
||||
right: 212px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.editor-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nodes-panel {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.properties-panel-container {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.blackboard-sidebar {
|
||||
width: 250px;
|
||||
max-height: 300px;
|
||||
top: 380px;
|
||||
right: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.blackboard-sidebar.collapsed {
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
flex: 1;
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 编辑器容器禁用状态 */
|
||||
.editor-container.disabled {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
opacity: 0.3;
|
||||
filter: blur(1px);
|
||||
}
|
||||
|
||||
/* 安装遮罩层 */
|
||||
.install-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(26, 32, 44, 0.95);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.overlay-content {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(20px);
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.overlay-icon {
|
||||
font-size: 60px;
|
||||
margin-bottom: 20px;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.overlay-content h3 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.overlay-content p {
|
||||
font-size: 16px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 30px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.overlay-actions {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.overlay-install-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px 28px;
|
||||
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.overlay-install-btn:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(34, 197, 94, 0.4);
|
||||
}
|
||||
|
||||
.overlay-install-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
}
|
||||
|
||||
.overlay-install-btn.installing {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
animation: installing-glow 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes installing-glow {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 10px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(59, 130, 246, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 2px solid white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.overlay-note {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.overlay-note small {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
/* 画布区域样式 */
|
||||
|
||||
.canvas-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #1a202c;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.canvas-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: #2d3748;
|
||||
border-bottom: 1px solid #4a5568;
|
||||
}
|
||||
|
||||
.zoom-controls, .canvas-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.zoom-controls button, .canvas-actions button {
|
||||
padding: 4px 8px;
|
||||
background: #4a5568;
|
||||
border: 1px solid #718096;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.zoom-controls button:hover, .canvas-actions button:hover {
|
||||
background: #667eea;
|
||||
}
|
||||
|
||||
.canvas-area {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #1a202c;
|
||||
background-image:
|
||||
radial-gradient(circle, #2d3748 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.behavior-tree-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 连接线样式 */
|
||||
.connection-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.connection-line {
|
||||
fill: none;
|
||||
stroke: #667eea;
|
||||
stroke-width: 2;
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.connection-line:hover {
|
||||
stroke: #f6ad55;
|
||||
stroke-width: 3;
|
||||
filter: drop-shadow(0 0 6px rgba(246, 173, 85, 0.6));
|
||||
}
|
||||
|
||||
.connection-active {
|
||||
stroke: #48bb78;
|
||||
stroke-dasharray: 8, 4;
|
||||
animation: flow 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes flow {
|
||||
0% { stroke-dashoffset: 0; }
|
||||
100% { stroke-dashoffset: 12; }
|
||||
}
|
||||
|
||||
.connection-temp {
|
||||
fill: none;
|
||||
stroke: #f6ad55;
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 5, 5;
|
||||
animation: dash 1s linear infinite;
|
||||
opacity: 0.8;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
0% { stroke-dashoffset: 0; }
|
||||
100% { stroke-dashoffset: 10; }
|
||||
}
|
||||
|
||||
/* 节点层 */
|
||||
.nodes-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
transform-origin: 0 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 网格背景 */
|
||||
.grid-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* 画布状态 */
|
||||
.canvas-area.connecting {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.canvas-area.connecting .tree-node {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.canvas-area.connecting .port {
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.drag-over {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border: 2px dashed #667eea;
|
||||
}
|
||||
@@ -1,359 +0,0 @@
|
||||
/* 条件拖拽功能样式 */
|
||||
|
||||
/* 可拖拽条件节点 */
|
||||
.node-item.draggable-condition {
|
||||
border-left: 3px solid #ffd700;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.node-item.draggable-condition .drag-hint {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 10px;
|
||||
color: #ffd700;
|
||||
animation: bounce-hint 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce-hint {
|
||||
0%, 20%, 50%, 80%, 100% {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-60%);
|
||||
}
|
||||
60% {
|
||||
transform: translateY(-40%);
|
||||
}
|
||||
}
|
||||
|
||||
/* 条件装饰器 - 基础状态 */
|
||||
.conditional-decorator {
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
max-width: 350px; /* 增加最大宽度以容纳长的条件名称 */
|
||||
min-height: 80px;
|
||||
transition: all 0.3s ease;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 条件装饰器 - 有附加条件状态 */
|
||||
.conditional-decorator.has-attached-condition {
|
||||
width: auto; /* 自动调整宽度 */
|
||||
min-width: 280px; /* 进一步增加最小宽度,确保较长的条件名称能完整显示 */
|
||||
max-width: 400px; /* 增加最大宽度 */
|
||||
min-height: 110px; /* 增加基础高度 */
|
||||
}
|
||||
|
||||
/* 条件装饰器 - 展开状态 */
|
||||
.conditional-decorator.has-attached-condition .condition-properties {
|
||||
min-height: 30px; /* 为展开内容预留空间 */
|
||||
}
|
||||
|
||||
/* 条件装饰器接受状态 */
|
||||
.tree-node.can-accept-condition {
|
||||
border: 2px dashed #ffd700;
|
||||
animation: pulse-accept 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-accept {
|
||||
0%, 100% {
|
||||
border-color: #ffd700;
|
||||
box-shadow: 0 0 5px rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
50% {
|
||||
border-color: #ffed4e;
|
||||
box-shadow: 0 0 15px rgba(255, 215, 0, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node.condition-hover {
|
||||
border-color: #ffd700;
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* 条件吸附区域 */
|
||||
.condition-attachment-area {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.condition-placeholder {
|
||||
text-align: center;
|
||||
padding: 6px 8px;
|
||||
border: 2px dashed #4a5568;
|
||||
border-radius: 4px;
|
||||
color: #a0aec0;
|
||||
font-size: 10px;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tree-node.can-accept-condition .condition-placeholder {
|
||||
border-color: #ffd700;
|
||||
color: #ffd700;
|
||||
background: rgba(255, 215, 0, 0.05);
|
||||
animation: pulse-placeholder 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-placeholder {
|
||||
0%, 100% {
|
||||
background: rgba(255, 215, 0, 0.05);
|
||||
}
|
||||
50% {
|
||||
background: rgba(255, 215, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 附加的条件 */
|
||||
.attached-condition {
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
border: 1px solid #ffd700;
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
/* 条件信息区域 */
|
||||
.condition-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
padding: 4px 6px 4px 6px;
|
||||
padding-right: 40px; /* 为右侧两个按钮预留空间 */
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1.3;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.condition-info:hover {
|
||||
background: rgba(255, 215, 0, 0.2);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.condition-info.condition-selected {
|
||||
background: rgba(255, 215, 0, 0.3);
|
||||
border: 1px solid #ffd700;
|
||||
box-shadow: 0 0 0 1px rgba(255, 215, 0, 0.25);
|
||||
}
|
||||
|
||||
.condition-icon {
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.condition-text {
|
||||
flex: 1;
|
||||
color: #ffd700;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
line-height: 1.3;
|
||||
min-width: 0;
|
||||
max-width: calc(100% - 50px); /* 为图标、编辑提示和按钮预留空间 */
|
||||
white-space: nowrap; /* 尽量保持一行显示 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; /* 如果实在太长则显示省略号 */
|
||||
}
|
||||
|
||||
.edit-hint {
|
||||
font-size: 9px;
|
||||
color: #a0aec0;
|
||||
margin-left: auto;
|
||||
transition: color 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.condition-info:hover .edit-hint {
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
/* 展开/收缩按钮 */
|
||||
.toggle-condition-btn {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 20px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: none;
|
||||
background: #4a5568;
|
||||
color: #a0aec0;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
font-size: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.toggle-condition-btn:hover {
|
||||
background: #2d3748;
|
||||
color: #ffd700;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 移除条件按钮 */
|
||||
.remove-condition-btn {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
background: #e53e3e;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0.8;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.remove-condition-btn:hover {
|
||||
background: #c53030;
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 条件属性展开区域 */
|
||||
.condition-properties {
|
||||
margin-top: 6px;
|
||||
padding-top: 6px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-right: 6px; /* 为按钮预留一点空间 */
|
||||
}
|
||||
|
||||
/* 属性分隔线 */
|
||||
.properties-divider {
|
||||
width: calc(100% - 6px);
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent 0%, #ffd700 20%, #ffd700 80%, transparent 100%);
|
||||
margin-bottom: 6px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 条件属性项 */
|
||||
.condition-property-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 2px 4px;
|
||||
margin-bottom: 2px;
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.condition-property-label {
|
||||
color: #a0aec0;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.condition-property-value {
|
||||
color: #ffd700;
|
||||
font-weight: 400;
|
||||
text-align: right;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
white-space: normal; /* 允许值换行 */
|
||||
}
|
||||
|
||||
/* 画布状态 */
|
||||
.canvas-area.condition-dragging {
|
||||
background: rgba(255, 215, 0, 0.02);
|
||||
}
|
||||
|
||||
.canvas-area.condition-dragging .tree-node:not(.can-accept-condition) {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 条件装饰器节点的特殊样式 */
|
||||
.tree-node.node-conditional-decorator {
|
||||
/* 基础高度和宽度 */
|
||||
min-height: 80px;
|
||||
width: 200px; /* 增加基础宽度 */
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 附加了条件的装饰器节点需要更大的高度 */
|
||||
.tree-node.node-conditional-decorator.has-attached-condition {
|
||||
min-height: 130px; /* 增加高度 */
|
||||
width: 220px; /* 进一步增加宽度以容纳更多内容 */
|
||||
}
|
||||
|
||||
.tree-node.node-conditional-decorator .condition-attachment-area {
|
||||
border: 1px solid #9f7aea;
|
||||
background: rgba(159, 122, 234, 0.05);
|
||||
margin-top: 4px;
|
||||
min-height: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 当有条件附加时,增加条件区域的高度 */
|
||||
.tree-node.node-conditional-decorator.has-attached-condition .condition-attachment-area {
|
||||
min-height: 45px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(255, 215, 0, 0.08);
|
||||
border-color: #ffd700;
|
||||
}
|
||||
|
||||
.tree-node.node-conditional-decorator.node-selected .condition-attachment-area {
|
||||
border-color: #ffd700;
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 条件附加成功的动画效果 */
|
||||
@keyframes condition-attached {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
background: rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
background: rgba(255, 215, 0, 0.5);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.condition-just-attached {
|
||||
animation: condition-attached 0.6s ease-out;
|
||||
}
|
||||
@@ -1,610 +0,0 @@
|
||||
/* 模态框和状态栏样式 */
|
||||
|
||||
/* 状态栏 */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: #2d3748;
|
||||
border-top: 1px solid #4a5568;
|
||||
font-size: 12px;
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.status-left, .status-right {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.status-valid {
|
||||
color: #48bb78;
|
||||
}
|
||||
|
||||
.status-invalid {
|
||||
color: #e53e3e;
|
||||
}
|
||||
|
||||
/* 模态框 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10000;
|
||||
animation: modalFadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
backdrop-filter: blur(0px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #2d3748;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.6);
|
||||
max-width: 95vw;
|
||||
max-height: 90vh;
|
||||
min-width: 800px;
|
||||
width: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #4a5568;
|
||||
animation: modalSlideIn 0.3s ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #4a5568;
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modal-header h3::before {
|
||||
content: '✨';
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.modal-header button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: none;
|
||||
color: #cbd5e0;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-header button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #ffffff;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
background: #2d3748;
|
||||
min-width: 750px;
|
||||
}
|
||||
|
||||
/* 导出选项样式增强 */
|
||||
.export-options {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid #4a5568;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.export-options label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid #4a5568;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.export-options label:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: #667eea;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
.export-options label:has(input[type="radio"]:checked) {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.export-options input[type="radio"] {
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #4a5568;
|
||||
border-radius: 50%;
|
||||
background: #1a202c;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.export-options input[type="radio"]:checked {
|
||||
border-color: #667eea;
|
||||
background: #667eea;
|
||||
}
|
||||
|
||||
.export-options input[type="radio"]:checked::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.export-options label span {
|
||||
color: #e2e8f0;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.export-options label small {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #a0aec0;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* 禁用选项样式 */
|
||||
.export-options label.disabled-option {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background: rgba(255, 255, 255, 0.01);
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.export-options label.disabled-option:hover {
|
||||
background: rgba(255, 255, 255, 0.01);
|
||||
border-color: #374151;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.export-options label.disabled-option span {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.export-options label.disabled-option input[type="radio"] {
|
||||
border-color: #374151;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.export-options label.disabled-option input[type="radio"]:disabled {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
.code-output {
|
||||
background: #1a202c;
|
||||
border: 1px solid #4a5568;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.code-output pre {
|
||||
margin: 0;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
color: #e2e8f0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 20px 24px;
|
||||
border-top: 1px solid #4a5568;
|
||||
justify-content: flex-end;
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.modal-footer button {
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-footer button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
.modal-footer button:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.modal-footer button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.modal-footer button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.modal-footer .copy-btn {
|
||||
background: linear-gradient(135deg, #3182ce 0%, #2c5282 100%);
|
||||
border: 1px solid #3182ce;
|
||||
}
|
||||
|
||||
.modal-footer .copy-btn:hover {
|
||||
background: linear-gradient(135deg, #2c5282 0%, #2a4365 100%);
|
||||
border-color: #2c5282;
|
||||
}
|
||||
|
||||
.modal-footer .save-file-btn {
|
||||
background: linear-gradient(135deg, #38a169 0%, #2f855a 100%);
|
||||
border: 1px solid #38a169;
|
||||
}
|
||||
|
||||
.modal-footer .save-file-btn:hover {
|
||||
background: linear-gradient(135deg, #2f855a 0%, #276749 100%);
|
||||
border-color: #2f855a;
|
||||
}
|
||||
|
||||
.modal-footer .close-btn {
|
||||
background: linear-gradient(135deg, #718096 0%, #4a5568 100%);
|
||||
border: 1px solid #718096;
|
||||
}
|
||||
|
||||
.modal-footer .close-btn:hover {
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Blackboard模态框特定样式 */
|
||||
.blackboard-modal {
|
||||
width: 520px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #e2e8f0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-group label::before {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||||
border: 2px solid #4a5568;
|
||||
border-radius: 8px;
|
||||
color: #e2e8f0;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-select:focus,
|
||||
.form-textarea:focus {
|
||||
border-color: #667eea;
|
||||
outline: none;
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2), 0 4px 12px rgba(102, 126, 234, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.form-input:hover,
|
||||
.form-select:hover,
|
||||
.form-textarea:hover {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 美化表单下拉选择框 */
|
||||
.form-select {
|
||||
appearance: none;
|
||||
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 5"><path fill="%23718096" d="M2 0L0 2h4zm0 5L0 3h4z"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 16px center;
|
||||
background-size: 12px;
|
||||
padding-right: 48px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-select option {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.form-input::placeholder,
|
||||
.form-textarea::placeholder {
|
||||
color: #718096;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.constraint-inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid #4a5568;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.constraint-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.constraint-row label {
|
||||
min-width: 60px;
|
||||
font-size: 12px;
|
||||
color: #a0aec0;
|
||||
margin: 0;
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.constraint-row label::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 自定义复选框样式 */
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 16px 0;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 8px;
|
||||
border: 1px solid #4a5568;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.checkbox-group:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #4a5568;
|
||||
border-radius: 4px;
|
||||
background: #1a202c;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"]:checked {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"]:checked::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
margin: 0;
|
||||
color: #e2e8f0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-transform: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.checkbox-group label::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.constraint-row label {
|
||||
min-width: 60px;
|
||||
margin-bottom: 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.constraint-row input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.allowed-values textarea {
|
||||
margin-top: 8px;
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%);
|
||||
border: 2px solid #4a5568;
|
||||
border-radius: 8px;
|
||||
color: #e2e8f0;
|
||||
padding: 12px 16px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.allowed-values textarea:focus {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2), 0 4px 12px rgba(102, 126, 234, 0.15);
|
||||
transform: translateY(-1px);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.allowed-values textarea:hover {
|
||||
border-color: #667eea;
|
||||
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.allowed-values textarea::placeholder {
|
||||
color: #718096;
|
||||
font-style: italic;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.export-code {
|
||||
background: #1a202c;
|
||||
border: 1px solid #4a5568;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 11px;
|
||||
color: #e2e8f0;
|
||||
resize: none;
|
||||
min-height: 400px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
line-height: 1.4;
|
||||
overflow-y: auto;
|
||||
white-space: pre;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
/* 节点样式 */
|
||||
|
||||
.tree-node {
|
||||
position: absolute;
|
||||
width: 160px;
|
||||
min-height: 80px;
|
||||
background: #2d3748;
|
||||
border: 2px solid #4a5568;
|
||||
border-radius: 8px;
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 1;
|
||||
animation: nodeAppear 0.3s ease-out;
|
||||
}
|
||||
|
||||
.tree-node:hover {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tree-node.node-selected {
|
||||
border-color: #f6ad55;
|
||||
box-shadow: 0 0 20px rgba(246, 173, 85, 0.4);
|
||||
}
|
||||
|
||||
.tree-node.node-error {
|
||||
border-color: #e53e3e;
|
||||
background: rgba(229, 62, 62, 0.1);
|
||||
}
|
||||
|
||||
.node-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #4a5568;
|
||||
border-radius: 6px 6px 0 0;
|
||||
border-bottom: 1px solid #718096;
|
||||
}
|
||||
|
||||
.node-header .node-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.node-title {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: #e2e8f0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.node-delete {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #e53e3e;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.node-delete:hover {
|
||||
background: rgba(229, 62, 62, 0.2);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.node-body {
|
||||
padding: 8px 12px;
|
||||
color: #a0aec0;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
max-width: 136px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 条件装饰器节点的node-body需要更大的宽度 */
|
||||
.tree-node.node-conditional-decorator .node-body {
|
||||
max-width: 176px; /* 基础状态 */
|
||||
}
|
||||
|
||||
.tree-node.node-conditional-decorator.has-attached-condition .node-body {
|
||||
max-width: 196px; /* 附加条件后更宽 */
|
||||
}
|
||||
|
||||
.node-description {
|
||||
margin-bottom: 6px;
|
||||
color: #cbd5e0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 端口样式 */
|
||||
.port {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transform: translateX(-50%);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 10;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.port::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.port:hover {
|
||||
transform: translateX(-50%) scale(1.3);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.port-input {
|
||||
top: -6px;
|
||||
left: 50%;
|
||||
background: #4299e1;
|
||||
border: 2px solid #2b6cb0;
|
||||
}
|
||||
|
||||
.port-input:hover {
|
||||
background: #63b3ed;
|
||||
box-shadow: 0 0 12px rgba(66, 153, 225, 0.6);
|
||||
}
|
||||
|
||||
.port-output {
|
||||
bottom: -6px;
|
||||
left: 50%;
|
||||
background: #48bb78;
|
||||
border: 2px solid #2f855a;
|
||||
}
|
||||
|
||||
.port-output:hover {
|
||||
background: #68d391;
|
||||
box-shadow: 0 0 12px rgba(72, 187, 120, 0.6);
|
||||
}
|
||||
|
||||
.port.connecting {
|
||||
animation: pulse-port 1s ease-in-out infinite;
|
||||
transform: translateX(-50%) scale(1.4);
|
||||
}
|
||||
|
||||
@keyframes pulse-port {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 8px rgba(246, 173, 85, 0.4);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(246, 173, 85, 0.8);
|
||||
transform: translateX(-50%) scale(1.6);
|
||||
}
|
||||
}
|
||||
|
||||
.port.drag-target {
|
||||
animation: pulse-target 0.8s ease-in-out infinite;
|
||||
background: #f6ad55 !important;
|
||||
border-color: #dd6b20 !important;
|
||||
transform: translateX(-50%) scale(1.5);
|
||||
}
|
||||
|
||||
@keyframes pulse-target {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 12px rgba(246, 173, 85, 0.6);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 24px rgba(246, 173, 85, 1);
|
||||
transform: translateX(-50%) scale(1.7);
|
||||
}
|
||||
}
|
||||
|
||||
.port-inner {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.port:hover .port-inner {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.port.connecting .port-inner {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.port.drag-target .port-inner {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.children-indicator {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 6px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* 节点属性预览 */
|
||||
.node-properties-preview {
|
||||
margin-top: 6px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.property-preview-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
padding: 1px 0;
|
||||
}
|
||||
|
||||
.property-label {
|
||||
color: #a0aec0;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.property-value {
|
||||
color: #e2e8f0;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
/* 不同类型属性的颜色 */
|
||||
.property-value.property-boolean {
|
||||
color: #68d391;
|
||||
}
|
||||
|
||||
.property-value.property-number {
|
||||
color: #63b3ed;
|
||||
}
|
||||
|
||||
.property-value.property-select {
|
||||
color: #f6ad55;
|
||||
}
|
||||
|
||||
.property-value.property-string {
|
||||
color: #cbd5e0;
|
||||
}
|
||||
|
||||
.property-value.property-code {
|
||||
color: #d69e2e;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
/* 当节点有属性时稍微增加高度空间 */
|
||||
.tree-node:has(.node-properties-preview) {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
/* 节点悬停时的端口显示优化 */
|
||||
.tree-node:hover .port {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
.tree-node .port {
|
||||
opacity: 0.8;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* 拖动状态样式 - 优化硬件加速 */
|
||||
.tree-node.dragging {
|
||||
opacity: 0.9;
|
||||
transform: translateZ(0) scale(1.02) rotate(1deg);
|
||||
box-shadow: 0 12px 30px rgba(0,0,0,0.4);
|
||||
z-index: 1000;
|
||||
cursor: grabbing;
|
||||
will-change: transform, opacity;
|
||||
backface-visibility: hidden;
|
||||
transform-style: preserve-3d;
|
||||
transition: none !important;
|
||||
border-color: #67b7dc;
|
||||
transform-origin: center center;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.dragging {
|
||||
opacity: 0.7;
|
||||
transform: rotate(5deg);
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes nodeAppear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 节点类型提示 */
|
||||
.tree-node[data-original-name]:not([data-original-name=""]):hover::after {
|
||||
content: "原始: " attr(data-original-name);
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: #f7fafc;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
animation: tooltipFadeIn 0.3s ease forwards;
|
||||
border: 1px solid #4a5568;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.tree-node.node-selected[data-original-name]:not([data-original-name=""]) .node-header::before {
|
||||
content: "📝";
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 8px;
|
||||
border: 2px solid #2d3748;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
@keyframes tooltipFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 节点标题自定义指示器 */
|
||||
.node-title.customized {
|
||||
color: #f6ad55;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.node-title.customized::after {
|
||||
content: " ✏️";
|
||||
font-size: 10px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,299 +0,0 @@
|
||||
/* 头部工具栏样式 */
|
||||
.header-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-bottom: 2px solid #4a5568;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.toolbar-left h2 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.current-file {
|
||||
color: #a0aec0;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.unsaved-indicator {
|
||||
color: #ff6b6b;
|
||||
animation: pulse-unsaved 2s infinite;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@keyframes pulse-unsaved {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.toolbar-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tool-btn:hover:not(:disabled) {
|
||||
background: rgba(255,255,255,0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.tool-btn.active {
|
||||
background: rgba(102, 126, 234, 0.3);
|
||||
border-color: #667eea;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* 布局组样式 */
|
||||
.layout-group {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 8px;
|
||||
padding: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.layout-group button {
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(255,255,255,0.8);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.layout-group button:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.layout-group button:active {
|
||||
transform: translateY(0);
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tool-btn.has-changes {
|
||||
background: rgba(255, 107, 107, 0.2);
|
||||
border-color: #ff6b6b;
|
||||
animation: glow-save 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes glow-save {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 5px rgba(255, 107, 107, 0.5);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 15px rgba(255, 107, 107, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
/* 安装状态容器 */
|
||||
.install-status-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.install-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
border-radius: 12px;
|
||||
min-width: 280px;
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.install-status:hover {
|
||||
background: rgba(255,255,255,0.15);
|
||||
border-color: rgba(255,255,255,0.3);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* 状态信息 */
|
||||
.status-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.main-text {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.version-text {
|
||||
font-size: 11px;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
/* 安装按钮 */
|
||||
.install-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.install-btn:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.4);
|
||||
}
|
||||
|
||||
.install-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
|
||||
}
|
||||
|
||||
.install-btn.installing {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 加载点动画 */
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.loading-dots span {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
animation: loading-dots 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.loading-dots span:nth-child(1) { animation-delay: -0.32s; }
|
||||
.loading-dots span:nth-child(2) { animation-delay: -0.16s; }
|
||||
|
||||
@keyframes loading-dots {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 刷新按钮 */
|
||||
.refresh-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
background: rgba(255,255,255,0.2);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* 状态颜色 */
|
||||
.install-status.installed {
|
||||
border-color: rgba(34, 197, 94, 0.5);
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.install-status.not-installed {
|
||||
border-color: rgba(239, 68, 68, 0.5);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
.install-status.installing {
|
||||
border-color: rgba(59, 130, 246, 0.5);
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
animation: installing-pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes installing-pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 5px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 15px rgba(59, 130, 246, 0.6);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,483 +0,0 @@
|
||||
#text {
|
||||
color: var(--color-normal-contrast-weakest);
|
||||
margin: auto;
|
||||
width: 180px;
|
||||
}
|
||||
.counter {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ecs-welcome-panel {
|
||||
height: 100%;
|
||||
background: var(--color-panel-bg);
|
||||
color: var(--color-text-normal);
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.welcome-container {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header p {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.status-section {
|
||||
background: var(--color-area-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
padding: 14px;
|
||||
margin-bottom: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: var(--color-text-normal);
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--color-border-soft);
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
color: var(--color-text-contrast-weakest);
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-badge.installed {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.status-badge.not-installed {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.status-badge.checking {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-contrast-weak);
|
||||
}
|
||||
|
||||
.update-hint {
|
||||
color: #ff6b35;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
margin-left: 8px;
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.management-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.management-buttons ui-button {
|
||||
width: 100%;
|
||||
min-height: 32px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.management-buttons ui-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.management-buttons ui-button.green {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: 1px solid #28a745;
|
||||
}
|
||||
|
||||
.management-buttons ui-button.red {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border: 1px solid #dc3545;
|
||||
}
|
||||
|
||||
.management-buttons ui-button.blue {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: 1px solid #007bff;
|
||||
}
|
||||
|
||||
.operation-status {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.operation-status.loading {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.operation-status.success {
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.operation-status.error,
|
||||
.operation-status.failed {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.operation-status.warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-details {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
opacity: 0.8;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.actions-section {
|
||||
background: var(--color-area-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
padding: 14px;
|
||||
margin-bottom: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
background: var(--color-neutral-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-card:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.action-card h3 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-normal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-card p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-contrast-weakest);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.footer {
|
||||
background: var(--color-area-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: var(--color-text-contrast-weak);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.last-check {
|
||||
margin-top: 4px;
|
||||
font-size: 10px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid var(--color-border);
|
||||
border-top: 2px solid var(--color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 优化滚动条样式 */
|
||||
.welcome-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.welcome-container::-webkit-scrollbar-track {
|
||||
background: var(--color-area-bg);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.welcome-container::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border-emphasis);
|
||||
border-radius: 4px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.welcome-container::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-border-emphasis-strong);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 状态切换动画 */
|
||||
.status-badge {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
transform: translateY(0);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-card:hover:not(.disabled) {
|
||||
background: var(--color-area-bg-hover);
|
||||
border-color: var(--color-primary-highlight);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-card.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-card.disabled:hover {
|
||||
background: var(--color-neutral-bg);
|
||||
border-color: var(--color-border);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* action-card样式 */
|
||||
.action-card.special {
|
||||
background: linear-gradient(135deg, var(--color-primary-fill) 0%, var(--color-focus-fill) 100%);
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.action-card.special h3 {
|
||||
color: var(--color-primary-contrast);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.action-card.special p {
|
||||
color: var(--color-primary-contrast);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.action-card.special:hover {
|
||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-focus-border) 100%);
|
||||
border-color: var(--color-focus-border);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.action-card.special .icon {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* 响应式布局 */
|
||||
@media (max-width: 600px) {
|
||||
.welcome-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
min-width: auto;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.management-buttons ui-button {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 现有文件信息样式 */
|
||||
.existing-files-info {
|
||||
margin-top: 12px;
|
||||
padding: 10px;
|
||||
background: var(--color-area-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.files-summary {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-normal);
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.files-list,
|
||||
.files-overflow {
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
padding: 2px 0;
|
||||
color: var(--color-text-contrast-weak);
|
||||
font-family: var(--font-mono);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.more-files {
|
||||
padding: 4px 0;
|
||||
color: var(--color-text-contrast-weakest);
|
||||
font-style: italic;
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin-top: 4px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
/* 文件列表滚动条样式 */
|
||||
.files-list::-webkit-scrollbar,
|
||||
.files-overflow::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.files-list::-webkit-scrollbar-track,
|
||||
.files-overflow::-webkit-scrollbar-track {
|
||||
background: var(--color-area-bg);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.files-list::-webkit-scrollbar-thumb,
|
||||
.files-overflow::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.files-list::-webkit-scrollbar-thumb:hover,
|
||||
.files-overflow::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-border-emphasis);
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
/* ECS代码生成器样式 */
|
||||
.ecs-generator {
|
||||
height: 100%;
|
||||
background: var(--color-panel-bg);
|
||||
color: var(--color-text-normal);
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.generator-container {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
max-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.generator-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.generator-header h1 {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.generator-header p {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 表单区域 */
|
||||
.generator-form {
|
||||
background: var(--color-area-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: var(--color-text-normal);
|
||||
}
|
||||
|
||||
/* 输入字段 */
|
||||
.input-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-normal);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.feature-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
background: var(--color-panel-bg);
|
||||
color: var(--color-text-normal);
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.feature-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.input-hint {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-contrast-weakest);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 选项组 */
|
||||
.options-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.option-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 复选框样式 */
|
||||
.checkbox-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.checkbox-wrapper input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.option-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-normal);
|
||||
}
|
||||
|
||||
.option-description {
|
||||
margin-left: 24px;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-contrast-weakest);
|
||||
}
|
||||
|
||||
/* 系统配置区域 */
|
||||
.system-config {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background: var(--color-panel-bg);
|
||||
border: 1px solid var(--color-border-soft);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.config-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.config-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-normal);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 系统类型卡片 */
|
||||
.system-type-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.system-card {
|
||||
padding: 16px;
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
background: var(--color-area-bg);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.system-card:hover {
|
||||
border-color: #667eea;
|
||||
background: var(--color-panel-bg);
|
||||
}
|
||||
|
||||
.system-card.active {
|
||||
border-color: #667eea;
|
||||
background: var(--color-panel-bg);
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.system-card.active::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 12px;
|
||||
color: #667eea;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-normal);
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-contrast-weak);
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.card-usage {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-contrast-weakest);
|
||||
font-style: italic;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* 系统类型选择 */
|
||||
.system-type-group {
|
||||
margin-left: 24px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.system-type-select {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
background: var(--color-panel-bg);
|
||||
color: var(--color-text-normal);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 预览区域 */
|
||||
.preview-section {
|
||||
background: var(--color-area-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
flex: 1;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: var(--color-text-normal);
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
background: var(--color-panel-bg);
|
||||
border: 1px solid var(--color-border-soft);
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-contrast-weak);
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* 操作按钮区域 */
|
||||
.action-section {
|
||||
background: var(--color-area-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.generate-btn {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.generate-btn:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.generate-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.btn-icon.spinning {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.generator-container::-webkit-scrollbar,
|
||||
.preview-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.generator-container::-webkit-scrollbar-track,
|
||||
.preview-content::-webkit-scrollbar-track {
|
||||
background: var(--color-area-bg);
|
||||
}
|
||||
|
||||
.generator-container::-webkit-scrollbar-thumb,
|
||||
.preview-content::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.generator-container::-webkit-scrollbar-thumb:hover,
|
||||
.preview-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-border-emphasis);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 600px) {
|
||||
.generator-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.generator-header h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.feature-input {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,903 +0,0 @@
|
||||
<!-- 头部工具栏 -->
|
||||
<div class="header-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<h2>🌳 行为树可视化编辑器
|
||||
<span v-if="currentFileName" class="current-file">- {{ currentFileName }}</span>
|
||||
<span v-if="hasUnsavedChanges" class="unsaved-indicator">●</span>
|
||||
</h2>
|
||||
<div class="toolbar-buttons">
|
||||
<button class="tool-btn" @click="newBehaviorTree" title="新建行为树">
|
||||
<span>📄</span> 新建
|
||||
</button>
|
||||
<button class="tool-btn" :class="{ 'has-changes': hasUnsavedChanges }" @click="saveBehaviorTree" :title="currentFilePath ? '保存到: ' + currentFilePath : (currentFileName ? '另存为 ' + currentFileName : '保存行为树')">
|
||||
<span>💾</span> 保存{{ hasUnsavedChanges ? ' *' : '' }}
|
||||
</button>
|
||||
<button class="tool-btn" @click="saveAsBehaviorTree" title="另存为新文件">
|
||||
<span>💾</span> 另存为
|
||||
</button>
|
||||
<button class="tool-btn" @click="loadBehaviorTree" title="加载行为树">
|
||||
<span>📂</span> 加载
|
||||
</button>
|
||||
<button class="tool-btn" @click="exportConfig" title="导出配置">
|
||||
<span>⚡</span> 导出配置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<div class="install-status-container">
|
||||
<div class="install-status" :class="installStatusClass()">
|
||||
<div class="status-info">
|
||||
<span class="status-icon">{{ isInstalled ? '✅' : (isInstalling ? '⏳' : '❌') }}</span>
|
||||
<div class="status-text">
|
||||
<span class="main-text">{{ installStatusText() }}</span>
|
||||
<span v-if="version && isInstalled" class="version-text">版本 {{ version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
v-if="!isInstalled"
|
||||
@click="handleInstall"
|
||||
:disabled="isInstalling"
|
||||
class="install-btn"
|
||||
:class="{ 'installing': isInstalling }"
|
||||
>
|
||||
<span class="btn-icon">{{ isInstalling ? '⏳' : '📦' }}</span>
|
||||
<span class="btn-text">{{ isInstalling ? '安装中...' : '立即安装' }}</span>
|
||||
<div v-if="isInstalling" class="loading-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
v-if="isInstalled && !checkingStatus"
|
||||
@click="checkInstallStatus"
|
||||
class="refresh-btn"
|
||||
title="检查更新"
|
||||
>
|
||||
🔄
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-container" :class="{ 'disabled': !isInstalled }">
|
||||
<!-- 未安装遮罩 -->
|
||||
<div v-if="!isInstalled" class="install-overlay">
|
||||
<div class="overlay-content">
|
||||
<div class="overlay-icon">🤖</div>
|
||||
<h3>需要安装AI系统</h3>
|
||||
<p>行为树编辑器需要安装AI系统才能正常使用</p>
|
||||
<div class="overlay-actions">
|
||||
<button
|
||||
@click="handleInstall"
|
||||
:disabled="isInstalling"
|
||||
class="overlay-install-btn"
|
||||
:class="{ 'installing': isInstalling }"
|
||||
>
|
||||
<span>{{ isInstalling ? '⏳ 安装中...' : '📦 立即安装AI系统' }}</span>
|
||||
<div v-if="isInstalling" class="loading-spinner"></div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="overlay-note">
|
||||
<small>📝 安装完成后编辑器将自动可用</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 左侧节点面板 -->
|
||||
<div class="nodes-panel">
|
||||
<div class="panel-header">
|
||||
<h3>📦 节点库</h3>
|
||||
<input
|
||||
type="text"
|
||||
v-model="nodeSearchText"
|
||||
placeholder="搜索节点..."
|
||||
class="search-input"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="node-categories">
|
||||
<!-- 根节点 -->
|
||||
<div class="category">
|
||||
<h4 class="category-title">🌳 根节点</h4>
|
||||
<div class="node-list">
|
||||
<div
|
||||
v-for="node in filteredRootNodes()"
|
||||
:key="node.type"
|
||||
class="node-item root"
|
||||
:draggable="true"
|
||||
@dragstart="onNodeDragStart($event, node)"
|
||||
:title="node.description"
|
||||
>
|
||||
<span class="node-icon">{{ node.icon }}</span>
|
||||
<span class="node-name">{{ node.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 复合节点 -->
|
||||
<div class="category">
|
||||
<h4 class="category-title">🔗 复合节点</h4>
|
||||
<div class="node-list">
|
||||
<div
|
||||
v-for="node in filteredCompositeNodes()"
|
||||
:key="node.type"
|
||||
class="node-item composite"
|
||||
:draggable="true"
|
||||
@dragstart="onNodeDragStart($event, node)"
|
||||
:title="node.description"
|
||||
>
|
||||
<span class="node-icon">{{ node.icon }}</span>
|
||||
<span class="node-name">{{ node.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 装饰器节点 -->
|
||||
<div class="category">
|
||||
<h4 class="category-title">🎭 装饰器</h4>
|
||||
<div class="node-list">
|
||||
<div
|
||||
v-for="node in filteredDecoratorNodes()"
|
||||
:key="node.type"
|
||||
class="node-item decorator"
|
||||
:draggable="true"
|
||||
@dragstart="onNodeDragStart($event, node)"
|
||||
:title="node.description"
|
||||
>
|
||||
<span class="node-icon">{{ node.icon }}</span>
|
||||
<span class="node-name">{{ node.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 动作节点 -->
|
||||
<div class="category">
|
||||
<h4 class="category-title">⚡ 动作节点</h4>
|
||||
<div class="node-list">
|
||||
<div
|
||||
v-for="node in filteredActionNodes()"
|
||||
:key="node.type"
|
||||
class="node-item action"
|
||||
:draggable="true"
|
||||
@dragstart="onNodeDragStart($event, node)"
|
||||
:title="node.description"
|
||||
>
|
||||
<span class="node-icon">{{ node.icon }}</span>
|
||||
<span class="node-name">{{ node.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 条件节点 -->
|
||||
<div class="category">
|
||||
<h4 class="category-title">❓ 条件节点</h4>
|
||||
<div class="node-list">
|
||||
<div
|
||||
v-for="node in filteredConditionNodes()"
|
||||
:key="node.type"
|
||||
class="node-item condition"
|
||||
:class="{ 'draggable-condition': node.isDraggableCondition }"
|
||||
:draggable="true"
|
||||
@dragstart="handleConditionNodeDragStart($event, node)"
|
||||
:title="node.description + (node.isDraggableCondition ? ' (可拖拽到条件装饰器)' : '')"
|
||||
>
|
||||
<span class="node-icon">{{ node.icon }}</span>
|
||||
<span class="node-name">{{ node.name }}</span>
|
||||
<span v-if="node.isDraggableCondition" class="drag-hint">🎯</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ECS节点 -->
|
||||
<div class="category">
|
||||
<h4 class="category-title">🎮 ECS节点</h4>
|
||||
<div class="node-list">
|
||||
<div
|
||||
v-for="node in filteredECSNodes()"
|
||||
:key="node.type"
|
||||
class="node-item ecs"
|
||||
:draggable="true"
|
||||
@dragstart="onNodeDragStart($event, node)"
|
||||
:title="node.description"
|
||||
>
|
||||
<span class="node-icon">{{ node.icon }}</span>
|
||||
<span class="node-name">{{ node.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间画布区域 -->
|
||||
<div class="canvas-container">
|
||||
<div class="canvas-toolbar">
|
||||
<div class="zoom-controls">
|
||||
<button @click="zoomIn">🔍+</button>
|
||||
<span>{{ Math.round(zoomLevel * 100) }}%</span>
|
||||
<button @click="zoomOut">🔍-</button>
|
||||
<button @click="resetZoom">重置</button>
|
||||
</div>
|
||||
<div class="canvas-actions">
|
||||
<button @click="centerView">居中</button>
|
||||
<button @click="autoLayout" title="紧凑子树布局 - 子节点紧贴父节点">自动布局</button>
|
||||
<button @click="validateTree">验证</button>
|
||||
<button @click="clearAllConnections" title="清除所有连接线" v-if="connections.length > 0">清除连线</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="canvasAreaRef"
|
||||
class="canvas-area"
|
||||
:class="{
|
||||
'connecting': dragState.isConnecting,
|
||||
'condition-dragging': conditionDragState.isDraggingCondition
|
||||
}"
|
||||
@drop="handleCanvasDrop"
|
||||
@dragover="onCanvasDragOver"
|
||||
@wheel="onCanvasWheel"
|
||||
@mousedown="onCanvasMouseDown"
|
||||
@mousemove="onCanvasMouseMove"
|
||||
@mouseup="onCanvasMouseUp"
|
||||
>
|
||||
<canvas
|
||||
:width="canvasWidth"
|
||||
:height="canvasHeight"
|
||||
class="behavior-tree-canvas"
|
||||
></canvas>
|
||||
|
||||
<!-- 连接线绘制层 -->
|
||||
<svg
|
||||
ref="svgRef"
|
||||
class="connection-layer"
|
||||
:width="canvasWidth"
|
||||
:height="canvasHeight"
|
||||
>
|
||||
<g :transform="'translate(' + panX + ', ' + panY + ') scale(' + zoomLevel + ')'">
|
||||
<path
|
||||
v-for="connection in connections"
|
||||
:key="connection.id"
|
||||
:d="connection.path"
|
||||
class="connection-line"
|
||||
:class="{ 'connection-active': connection.active }"
|
||||
@click="onConnectionClick($event, connection.id)"
|
||||
:title="'点击删除连接线 (' + connection.sourceId + ' → ' + connection.targetId + ')'"
|
||||
/>
|
||||
<!-- 临时连接线 -->
|
||||
<path
|
||||
v-if="connectionState.tempPath"
|
||||
:d="connectionState.tempPath"
|
||||
class="connection-temp"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<!-- 节点渲染层 -->
|
||||
<div
|
||||
class="nodes-layer"
|
||||
:style="{ transform: 'translate(' + panX + 'px, ' + panY + 'px) scale(' + zoomLevel + ')' }"
|
||||
>
|
||||
<div
|
||||
v-for="node in treeNodes"
|
||||
:key="node.id"
|
||||
:data-node-id="node.id"
|
||||
:data-original-name="isNodeNameCustomized(node) ? getOriginalNodeName(node.type) : ''"
|
||||
class="tree-node"
|
||||
:class="[
|
||||
'node-' + node.type,
|
||||
{
|
||||
'node-selected': selectedNodeId === node.id,
|
||||
'node-error': node.hasError,
|
||||
'dragging': dragState.dragNode && dragState.dragNode.id === node.id,
|
||||
'condition-hover': conditionDragState.hoveredDecoratorId === node.id,
|
||||
'can-accept-condition': canAcceptCondition(node) && conditionDragState.isDraggingCondition,
|
||||
'has-attached-condition': node.type === 'conditional-decorator' && node.attachedCondition
|
||||
}
|
||||
]"
|
||||
:style="{
|
||||
left: node.x + 'px',
|
||||
top: node.y + 'px'
|
||||
}"
|
||||
@click="selectNode(node.id)"
|
||||
@mousedown="startNodeDrag($event, node)"
|
||||
@drop="handleNodeDrop($event, node)"
|
||||
@dragover="handleNodeDragOver($event, node)"
|
||||
@dragleave="handleNodeDragLeave($event, node)"
|
||||
>
|
||||
<div class="node-header">
|
||||
<span class="node-icon">{{ node.icon }}</span>
|
||||
<span class="node-title" :class="{ 'customized': isNodeNameCustomized(node) }">{{ node.name }}</span>
|
||||
<button class="node-delete" @click.stop="deleteNode(node.id)">×</button>
|
||||
</div>
|
||||
<div class="node-body">
|
||||
<div v-if="node.description" class="node-description">{{ node.description }}</div>
|
||||
|
||||
<!-- 条件装饰器的条件显示 -->
|
||||
<div v-if="node.type === 'conditional-decorator'" class="condition-attachment-area">
|
||||
<div v-if="node.attachedCondition" class="attached-condition">
|
||||
<div
|
||||
class="condition-info"
|
||||
:class="{ 'condition-selected': selectedConditionNodeId === node.id }"
|
||||
@click.stop="selectConditionNode(node)"
|
||||
title="点击编辑条件属性"
|
||||
>
|
||||
<span class="condition-icon">{{ node.attachedCondition.icon }}</span>
|
||||
<span class="condition-text">{{ getConditionDisplayText(node) }}</span>
|
||||
<span class="edit-hint">📝</span>
|
||||
</div>
|
||||
|
||||
<!-- 展开时显示条件属性 -->
|
||||
<div v-if="node.conditionExpanded" class="condition-properties">
|
||||
<div class="properties-divider"></div>
|
||||
<div
|
||||
v-for="(value, key) in getConditionProperties(node)"
|
||||
:key="key"
|
||||
class="condition-property-item"
|
||||
>
|
||||
<span class="condition-property-label">{{ key }}:</span>
|
||||
<span class="condition-property-value">{{ value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="toggle-condition-btn"
|
||||
@click.stop="toggleConditionExpanded(node)"
|
||||
:title="node.conditionExpanded ? '收缩详情' : '展开详情'"
|
||||
>{{ node.conditionExpanded ? '▲' : '▼' }}</button>
|
||||
<button
|
||||
class="remove-condition-btn"
|
||||
@click.stop="removeConditionFromDecorator(node)"
|
||||
title="移除条件"
|
||||
>×</button>
|
||||
</div>
|
||||
<div v-else class="condition-placeholder">
|
||||
<span class="placeholder-text">🎯 拖拽条件到此处</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 节点属性预览 -->
|
||||
<div v-if="hasVisibleProperties(node)" class="node-properties-preview">
|
||||
<div
|
||||
v-for="(prop, key) in getVisibleProperties(node)"
|
||||
:key="key"
|
||||
class="property-preview-item"
|
||||
:title="prop.name + ': ' + prop.description"
|
||||
>
|
||||
<span class="property-label">{{ prop.name }}:</span>
|
||||
<span class="property-value" :class="'property-' + prop.type">
|
||||
{{ formatPropertyValue(prop) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入端口 -->
|
||||
<div
|
||||
v-if="node.canHaveParent"
|
||||
class="port port-input"
|
||||
:class="{
|
||||
'connecting': connectionState.isConnecting &&
|
||||
connectionState.startPortType === 'output' &&
|
||||
connectionState.startNodeId !== node.id,
|
||||
'drag-target': isValidConnectionTarget(node.id, 'input')
|
||||
}"
|
||||
@mousedown.stop="startConnection($event, node.id, 'input')"
|
||||
@mouseenter="onPortHover(node.id, 'input')"
|
||||
@mouseleave="onPortLeave()"
|
||||
title="执行流入口"
|
||||
>
|
||||
<div class="port-inner"></div>
|
||||
</div>
|
||||
|
||||
<!-- 输出端口 -->
|
||||
<div
|
||||
v-if="node.canHaveChildren"
|
||||
class="port port-output"
|
||||
:class="{
|
||||
'connecting': connectionState.isConnecting &&
|
||||
connectionState.startPortType === 'input' &&
|
||||
connectionState.startNodeId !== node.id,
|
||||
'drag-target': isValidConnectionTarget(node.id, 'output')
|
||||
}"
|
||||
@mousedown.stop="startConnection($event, node.id, 'output')"
|
||||
@mouseenter="onPortHover(node.id, 'output')"
|
||||
@mouseleave="onPortLeave()"
|
||||
title="执行流出口"
|
||||
>
|
||||
<div class="port-inner"></div>
|
||||
</div>
|
||||
|
||||
<div v-if="node.children && node.children.length > 0" class="children-indicator">
|
||||
{{ node.children.length }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid-background" :style="gridStyle()"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="properties-panel-container">
|
||||
|
||||
<div class="properties-panel">
|
||||
<div class="panel-header">
|
||||
<h3>⚙️ 属性面板</h3>
|
||||
</div>
|
||||
|
||||
<div v-if="activeNode" class="node-properties">
|
||||
<!-- 节点类型信息区域 -->
|
||||
<div class="property-section node-type-info">
|
||||
<h4>
|
||||
<span class="node-type-icon">{{ activeNode.icon }}</span>
|
||||
节点类型信息
|
||||
<button
|
||||
class="reset-name-btn"
|
||||
@click="resetNodeToOriginalName"
|
||||
:title="'重置为原始名称: ' + getOriginalNodeName(activeNode.type)"
|
||||
v-if="isNodeNameCustomized(activeNode)"
|
||||
>
|
||||
🔄 恢复原名
|
||||
</button>
|
||||
</h4>
|
||||
<div class="node-type-details">
|
||||
<div class="type-info-row">
|
||||
<span class="info-label">原始类型:</span>
|
||||
<span class="info-value original-type">
|
||||
{{ getOriginalNodeName(activeNode.type) }}
|
||||
<span class="type-badge" :class="'badge-' + getNodeCategory(activeNode.type)">
|
||||
{{ getNodeCategory(activeNode.type) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="type-info-row">
|
||||
<span class="info-label">节点ID:</span>
|
||||
<span class="info-value node-id">{{ activeNode.type }}</span>
|
||||
</div>
|
||||
<div class="type-info-row" v-if="getNodeTemplate(activeNode.type)">
|
||||
<span class="info-label">原始描述:</span>
|
||||
<span class="info-value original-description">{{ getNodeTemplate(activeNode.type).description }}</span>
|
||||
</div>
|
||||
<div class="type-info-row" v-if="isNodeNameCustomized(activeNode)">
|
||||
<span class="info-label">⚠️ 自定义名称:</span>
|
||||
<span class="info-value custom-name">{{ activeNode.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property-section">
|
||||
<h4>基本信息</h4>
|
||||
<div class="property-item">
|
||||
<label>节点名称:</label>
|
||||
<div class="name-input-container">
|
||||
<input
|
||||
type="text"
|
||||
:value="activeNode.name"
|
||||
@input="updateNodeProperty('name', $event.target.value)"
|
||||
:key="activeNode.id + '_name'"
|
||||
:disabled="activeNode.isConditionNode"
|
||||
:placeholder="getOriginalNodeName(activeNode.type)"
|
||||
>
|
||||
<span v-if="isNodeNameCustomized(activeNode)" class="custom-indicator" title="已自定义名称">✏️</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="property-item">
|
||||
<label>描述:</label>
|
||||
<textarea
|
||||
:value="activeNode.description"
|
||||
@input="updateNodeProperty('description', $event.target.value)"
|
||||
:key="activeNode.id + '_description'"
|
||||
:disabled="activeNode.isConditionNode"
|
||||
:placeholder="getNodeTemplate(activeNode.type)?.description || '请输入节点描述...'"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property-section" v-if="activeNode.properties">
|
||||
<h4>节点属性</h4>
|
||||
<div
|
||||
v-for="(prop, key) in activeNode.properties"
|
||||
:key="key"
|
||||
class="property-item"
|
||||
:class="{ 'blackboard-droppable': isBlackboardDroppable(prop) }"
|
||||
@drop="handleBlackboardDrop($event, key)"
|
||||
@dragover="handleBlackboardDragOver"
|
||||
@dragleave="handleBlackboardDragLeave"
|
||||
>
|
||||
<label>{{ prop.name }}:</label>
|
||||
<div class="property-input-container">
|
||||
<!-- Blackboard 拖拽目标区域 -->
|
||||
<div
|
||||
v-if="isBlackboardDroppable(prop)"
|
||||
class="blackboard-drop-zone"
|
||||
:class="{ 'has-reference': isBlackboardReference(prop.value) }"
|
||||
@drop="handleBlackboardDrop($event, key)"
|
||||
@dragover="handleBlackboardDragOver"
|
||||
@dragleave="handleBlackboardDragLeave"
|
||||
>
|
||||
<div v-if="!isBlackboardReference(prop.value)" class="drop-placeholder">
|
||||
<span class="drop-icon">📋</span>
|
||||
<span class="drop-text">拖拽Blackboard变量到此处</span>
|
||||
</div>
|
||||
<div v-else class="blackboard-reference">
|
||||
<span class="ref-icon">🔗</span>
|
||||
<span class="ref-text">{{ prop.value }}</span>
|
||||
<button class="clear-ref" @click="clearBlackboardReference(key)" title="清除引用">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入控件 -->
|
||||
<input
|
||||
v-if="prop.type === 'string'"
|
||||
type="text"
|
||||
:value="prop.value"
|
||||
@input="updateNodeProperty('properties.' + key + '.value', $event.target.value)"
|
||||
:key="activeNode.id + '_' + key + '_string'"
|
||||
:placeholder="isBlackboardDroppable(prop) ? '或直接输入值' : '请输入值'"
|
||||
:class="{ 'with-blackboard': isBlackboardDroppable(prop) }"
|
||||
>
|
||||
<input
|
||||
v-else-if="prop.type === 'number'"
|
||||
type="number"
|
||||
:value="prop.value"
|
||||
@input="updateNodeProperty('properties.' + key + '.value', parseFloat($event.target.value) || 0)"
|
||||
:key="activeNode.id + '_' + key + '_number'"
|
||||
:placeholder="isBlackboardDroppable(prop) ? '或直接输入值' : '请输入值'"
|
||||
:class="{ 'with-blackboard': isBlackboardDroppable(prop) }"
|
||||
>
|
||||
<input
|
||||
v-else-if="prop.type === 'boolean'"
|
||||
type="checkbox"
|
||||
:checked="prop.value"
|
||||
@change="updateNodeProperty('properties.' + key + '.value', $event.target.checked)"
|
||||
:key="activeNode.id + '_' + key + '_boolean'"
|
||||
>
|
||||
<textarea
|
||||
v-else-if="prop.type === 'code'"
|
||||
:value="prop.value"
|
||||
@input="updateNodeProperty('properties.' + key + '.value', $event.target.value)"
|
||||
rows="6"
|
||||
class="code-input"
|
||||
placeholder="请输入代码..."
|
||||
:key="activeNode.id + '_' + key + '_code'"
|
||||
></textarea>
|
||||
<select
|
||||
v-else-if="prop.type === 'select'"
|
||||
:value="prop.value"
|
||||
@change="updateNodeProperty('properties.' + key + '.value', $event.target.value)"
|
||||
:key="activeNode.id + '_' + key + '_select_' + prop.value"
|
||||
>
|
||||
<option
|
||||
v-for="option in prop.options"
|
||||
:key="option"
|
||||
:value="option"
|
||||
:selected="option === prop.value"
|
||||
>
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<p v-if="prop.description" class="property-help">{{ prop.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="property-section">
|
||||
<h4>节点配置</h4>
|
||||
<pre class="config-preview">{{ activeNode ? JSON.stringify(activeNode, null, 2) : '{}' }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="no-selection">
|
||||
<p>请选择一个节点查看属性</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tree-structure-panel" v-if="rootNode()">
|
||||
<div class="panel-header">
|
||||
<h3>🌲 树结构</h3>
|
||||
</div>
|
||||
<div class="tree-view">
|
||||
<tree-node-item
|
||||
:node="rootNode()"
|
||||
:level="0"
|
||||
:get-node-by-id-local="getNodeByIdLocal"
|
||||
@node-select="selectNode"
|
||||
></tree-node-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="!validationResult().isValid" class="validation-error">
|
||||
<span class="error-icon">⚠️</span>
|
||||
<span>{{ validationResult().message }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="showBlackboardModal" class="modal-overlay" @click="showBlackboardModal = false">
|
||||
<div class="modal-content blackboard-modal" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3>{{ editingBlackboardVariable ? '编辑变量' : '添加变量' }}</h3>
|
||||
<button @click="showBlackboardModal = false">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>变量名称:</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="blackboardModalData.name"
|
||||
placeholder="例如:playerHealth、enemyCount..."
|
||||
:disabled="editingBlackboardVariable"
|
||||
class="form-input"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>变量类型:</label>
|
||||
<select v-model="blackboardModalData.type" class="form-select">
|
||||
<option value="string">📝 字符串 (string)</option>
|
||||
<option value="number">🔢 数字 (number)</option>
|
||||
<option value="boolean">☑️ 布尔值 (boolean)</option>
|
||||
<option value="vector2">📐 二维向量 (vector2)</option>
|
||||
<option value="vector3">📏 三维向量 (vector3)</option>
|
||||
<option value="object">📦 对象 (object)</option>
|
||||
<option value="array">📋 数组 (array)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>默认值:</label>
|
||||
<input
|
||||
v-if="blackboardModalData.type === 'string'"
|
||||
type="text"
|
||||
v-model="blackboardModalData.defaultValue"
|
||||
placeholder="输入默认字符串值..."
|
||||
class="form-input"
|
||||
>
|
||||
<input
|
||||
v-else-if="blackboardModalData.type === 'number'"
|
||||
type="number"
|
||||
v-model="blackboardModalData.defaultValue"
|
||||
placeholder="输入默认数值..."
|
||||
class="form-input"
|
||||
>
|
||||
<div v-else-if="blackboardModalData.type === 'boolean'" class="checkbox-group">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="blackboardModalData.defaultValue"
|
||||
id="defaultBoolValue"
|
||||
>
|
||||
<label for="defaultBoolValue">默认为 True</label>
|
||||
</div>
|
||||
<textarea
|
||||
v-else
|
||||
v-model="blackboardModalData.defaultValue"
|
||||
placeholder="请输入JSON格式的默认值,例如:{"x": 0, "y": 0}"
|
||||
rows="3"
|
||||
class="form-textarea"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>描述:</label>
|
||||
<textarea
|
||||
v-model="blackboardModalData.description"
|
||||
placeholder="描述这个变量的用途和作用..."
|
||||
rows="2"
|
||||
class="form-textarea"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>分组:</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="blackboardModalData.group"
|
||||
placeholder="例如:Player、AI、Environment、Game..."
|
||||
class="form-input"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-if="blackboardModalData.type === 'number'" class="form-group">
|
||||
<label>数值约束:</label>
|
||||
<div class="constraint-inputs">
|
||||
<div class="constraint-row">
|
||||
<label>最小值:</label>
|
||||
<input type="number" v-model="blackboardModalData.constraints.min" placeholder="不限制" class="form-input">
|
||||
</div>
|
||||
<div class="constraint-row">
|
||||
<label>最大值:</label>
|
||||
<input type="number" v-model="blackboardModalData.constraints.max" placeholder="不限制" class="form-input">
|
||||
</div>
|
||||
<div class="constraint-row">
|
||||
<label>步长:</label>
|
||||
<input type="number" v-model="blackboardModalData.constraints.step" placeholder="1" class="form-input">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="blackboardModalData.type === 'string'" class="form-group">
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" v-model="blackboardModalData.useAllowedValues" id="useAllowedValues">
|
||||
<label for="useAllowedValues">限制可选值</label>
|
||||
</div>
|
||||
<div v-if="blackboardModalData.useAllowedValues" class="allowed-values">
|
||||
<textarea
|
||||
v-model="blackboardModalData.allowedValuesText"
|
||||
placeholder="每行输入一个可选值 例如: idle running jumping attacking"
|
||||
rows="4"
|
||||
class="form-textarea"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" v-model="blackboardModalData.readOnly" id="readOnlyVar">
|
||||
<label for="readOnlyVar">只读变量</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button @click="saveBlackboardVariable" class="save-btn">保存</button>
|
||||
<button @click="showBlackboardModal = false" class="cancel-btn">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="blackboard-sidebar"
|
||||
:class="{
|
||||
'collapsed': blackboardCollapsed,
|
||||
'transparent': blackboardTransparent
|
||||
}"
|
||||
@mouseenter="blackboardTransparent = false"
|
||||
@mouseleave="blackboardTransparent = true">
|
||||
|
||||
<button class="blackboard-toggle"
|
||||
@click="blackboardCollapsed = !blackboardCollapsed"
|
||||
:title="blackboardCollapsed ? '展开 Blackboard' : '收缩 Blackboard'">
|
||||
<span v-if="blackboardCollapsed">📋</span>
|
||||
<span v-else>◀</span>
|
||||
</button>
|
||||
|
||||
<div class="blackboard-content" v-show="!blackboardCollapsed">
|
||||
<div class="blackboard-header">
|
||||
<h3>📋 Blackboard</h3>
|
||||
</div>
|
||||
|
||||
<div class="blackboard-actions">
|
||||
<button @click="showBlackboardModal = true" class="add-var-btn">+ 添加变量</button>
|
||||
<button @click="resetBlackboardToDefaults" v-if="blackboardVariables.length > 0" class="reset-btn" title="重置所有变量到默认值">🔄 重置</button>
|
||||
<button @click="clearBlackboard" v-if="blackboardVariables.length > 0" class="clear-btn">清空</button>
|
||||
</div>
|
||||
|
||||
<!-- 使用提示 -->
|
||||
<div v-if="blackboardVariables.length > 0" class="blackboard-usage-hint">
|
||||
<div class="hint-content">
|
||||
<span class="hint-icon">💡</span>
|
||||
<span class="hint-text">拖拽变量名使用</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="blackboard-scroll-area">
|
||||
<div v-if="blackboardVariables.length > 0" class="blackboard-groups">
|
||||
<div
|
||||
v-for="[groupName, variables] in groupedBlackboardVariables()"
|
||||
:key="groupName"
|
||||
class="variable-group"
|
||||
>
|
||||
<h4 class="group-title">{{ groupName }}</h4>
|
||||
<div class="variable-list">
|
||||
<div
|
||||
v-for="variable in variables"
|
||||
:key="variable.name"
|
||||
class="variable-item"
|
||||
:class="variable.type"
|
||||
:title="variable.description"
|
||||
>
|
||||
<div class="variable-header">
|
||||
<div
|
||||
class="variable-drag-area"
|
||||
:draggable="true"
|
||||
@dragstart="onBlackboardDragStart($event, variable)"
|
||||
@click.stop
|
||||
>
|
||||
<span class="variable-name">{{ variable.name }}</span>
|
||||
<span class="variable-type">{{ getTypeDisplayName(variable.type) }}</span>
|
||||
<span class="drag-hint">🎯</span>
|
||||
</div>
|
||||
<div class="variable-actions">
|
||||
<button @click.stop="editBlackboardVariable(variable)" class="edit-btn">✏️</button>
|
||||
<button @click.stop="removeBlackboardVariable(variable.name)" class="remove-btn">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="variable-value">
|
||||
<select
|
||||
v-if="variable.constraints && variable.constraints.allowedValues"
|
||||
v-model="variable.value"
|
||||
@change="onBlackboardValueChange(variable)"
|
||||
@click.stop
|
||||
:disabled="variable.readOnly"
|
||||
>
|
||||
<option
|
||||
v-for="option in variable.constraints.allowedValues"
|
||||
:key="option"
|
||||
:value="option"
|
||||
>
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-else class="value-display">{{ getDisplayValue(variable) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="variable.constraints && hasVisibleConstraints(variable)" class="variable-constraints">
|
||||
<small>{{ formatConstraints(variable.constraints) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-blackboard">
|
||||
<div class="empty-icon">📋</div>
|
||||
<p>还没有定义任何变量</p>
|
||||
<button class="add-first-var" @click="showBlackboardModal = true">添加第一个变量</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div v-if="showExportModal" class="modal-overlay" @click="showExportModal = false">
|
||||
<div class="modal-content" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3>⚡ 导出行为树配置</h3>
|
||||
<button @click="showExportModal = false">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="export-options">
|
||||
<h4 style="margin: 0 0 12px 0; color: #e2e8f0; font-size: 14px;">📄 选择导出格式:</h4>
|
||||
<label>
|
||||
<input type="radio" v-model="exportFormat" value="json">
|
||||
<span>📄 JSON配置文件</span>
|
||||
<small style="display: block; margin-left: 24px; color: #a0aec0; font-size: 11px;">适用于运行时动态加载行为树</small>
|
||||
</label>
|
||||
<label class="disabled-option">
|
||||
<input type="radio" v-model="exportFormat" value="typescript" disabled>
|
||||
<span>📝 TypeScript代码 (暂不可用)</span>
|
||||
<small style="display: block; margin-left: 24px; color: #6b7280; font-size: 11px;">此功能正在开发中,敬请期待</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="preview-section">
|
||||
<h4 style="margin: 0 0 8px 0; color: #e2e8f0; font-size: 13px;">📋 代码预览:</h4>
|
||||
<textarea
|
||||
class="export-code"
|
||||
:value="exportedCode()"
|
||||
readonly
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="usage-hint" style="margin-top: 16px; padding: 12px; background: rgba(102, 126, 234, 0.08); border: 1px solid rgba(102, 126, 234, 0.2); border-radius: 6px;">
|
||||
<div style="color: #a0aec0; font-size: 12px; line-height: 1.4;">
|
||||
<strong style="color: #e2e8f0;">💡 使用提示:</strong><br/>
|
||||
<span v-if="exportFormat === 'json'">
|
||||
• JSON配置可用于运行时动态加载行为树<br/>
|
||||
• 使用 BehaviorTreeBuilder.fromConfig() 方法构建行为树<br/>
|
||||
• 可以保存为 .json 文件在项目中使用
|
||||
</span>
|
||||
<span v-else>
|
||||
• 当前仅支持JSON格式导出<br/>
|
||||
• TypeScript代码生成功能正在开发中
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button @click="copyToClipboard" class="copy-btn">📋 复制到剪贴板</button>
|
||||
<button @click="saveToFile" class="save-file-btn">💾 保存到文件</button>
|
||||
<button @click="showExportModal = false" class="close-btn">❌ 关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,194 +0,0 @@
|
||||
<div class="blackboard-panel">
|
||||
<div class="panel-header">
|
||||
<h3>🎯 黑板变量</h3>
|
||||
<div class="header-actions">
|
||||
<button class="btn-add-variable" @click="showAddVariableDialog = true" title="添加新变量">
|
||||
➕ 添加变量
|
||||
</button>
|
||||
<button class="btn-import-export" @click="showImportExportDialog = true" title="导入/导出变量">
|
||||
📤 导入/导出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="variables-container">
|
||||
<!-- 按分组显示变量 -->
|
||||
<div v-for="group in groups" :key="group" class="variable-group">
|
||||
<div class="group-header" @click="toggleGroup(group)">
|
||||
<span class="group-icon">{{ expandedGroups.has(group) ? '📂' : '📁' }}</span>
|
||||
<span class="group-name">{{ group }}</span>
|
||||
<span class="group-count">({{ groupedBlackboardVariables()[group]?.length || 0 }})</span>
|
||||
</div>
|
||||
|
||||
<div v-if="expandedGroups.has(group)" class="group-variables">
|
||||
<div v-for="variable in groupedBlackboardVariables()[group]" :key="variable.name" class="variable-item"
|
||||
:class="[variable.type, { 'readonly': variable.readonly }]" :draggable="true"
|
||||
@dragstart="onBlackboardDragStart($event, variable)">
|
||||
|
||||
<div class="variable-header">
|
||||
<div class="variable-info">
|
||||
<span class="variable-name">{{ variable.name }}</span>
|
||||
<span class="value-separator">:</span>
|
||||
<span class="value-display">{{ getDisplayValue(variable) }}</span>
|
||||
</div>
|
||||
<span class="variable-type">{{ getTypeDisplayName(variable.type) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="variable-actions">
|
||||
<button @click="editVariable(variable)" title="编辑" class="edit-btn">✏</button>
|
||||
<button @click="removeBlackboardVariable(variable.name)" title="删除"
|
||||
class="remove-btn">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="groups.length === 0" class="empty-state">
|
||||
<div class="empty-icon">📋</div>
|
||||
<div class="empty-text">还没有黑板变量</div>
|
||||
<div class="empty-hint">点击"添加变量"来创建第一个变量</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加变量对话框 -->
|
||||
<div v-if="showAddVariableDialog" class="dialog-overlay" @click="closeAddVariableDialog">
|
||||
<div class="dialog" @click.stop>
|
||||
<div class="dialog-header">
|
||||
<h4>{{ editingVariable ? '编辑变量' : '添加变量' }}</h4>
|
||||
<button @click="closeAddVariableDialog" class="dialog-close">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="dialog-content">
|
||||
<div class="form-group">
|
||||
<label>变量名</label>
|
||||
<input v-model="newVariable.name" :disabled="editingVariable" type="text" placeholder="请输入变量名"
|
||||
class="form-input">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>变量类型</label>
|
||||
<select v-model="newVariable.type" class="form-select" @change="onTypeChange">
|
||||
<option value="string">字符串</option>
|
||||
<option value="number">数字</option>
|
||||
<option value="boolean">布尔</option>
|
||||
<option value="vector2">Vector2</option>
|
||||
<option value="vector3">Vector3</option>
|
||||
<option value="object">对象</option>
|
||||
<option value="array">数组</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>默认值</label>
|
||||
<!-- 根据类型显示不同的输入控件 -->
|
||||
<input v-if="newVariable.type === 'string'" v-model="newVariable.defaultValue" type="text"
|
||||
class="form-input">
|
||||
<input v-else-if="newVariable.type === 'number'" v-model.number="newVariable.defaultValue" type="number"
|
||||
class="form-input">
|
||||
<label v-else-if="newVariable.type === 'boolean'" class="checkbox-label">
|
||||
<input type="checkbox" v-model="newVariable.defaultValue">
|
||||
<span>{{ newVariable.defaultValue ? '真' : '假' }}</span>
|
||||
</label>
|
||||
<div v-else-if="newVariable.type === 'vector2'" class="vector-input">
|
||||
<input v-model.number="newVariable.defaultValue.x" type="number" placeholder="X">
|
||||
<input v-model.number="newVariable.defaultValue.y" type="number" placeholder="Y">
|
||||
</div>
|
||||
<div v-else-if="newVariable.type === 'vector3'" class="vector-input">
|
||||
<input v-model.number="newVariable.defaultValue.x" type="number" placeholder="X">
|
||||
<input v-model.number="newVariable.defaultValue.y" type="number" placeholder="Y">
|
||||
<input v-model.number="newVariable.defaultValue.z" type="number" placeholder="Z">
|
||||
</div>
|
||||
<textarea v-else-if="newVariable.type === 'object' || newVariable.type === 'array'"
|
||||
v-model="newVariable.defaultValueText" class="form-textarea" rows="3"
|
||||
placeholder="JSON格式"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>描述</label>
|
||||
<input v-model="newVariable.description" type="text" placeholder="变量的用途描述" class="form-input">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>分组</label>
|
||||
<input v-model="newVariable.group" type="text" placeholder="变量分组(如:Player、Enemy等)" class="form-input">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="newVariable.readonly">
|
||||
<span>只读</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数字类型的约束 -->
|
||||
<div v-if="newVariable.type === 'number'" class="form-row">
|
||||
<div class="form-group">
|
||||
<label>最小值</label>
|
||||
<input v-model.number="newVariable.min" type="number" class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>最大值</label>
|
||||
<input v-model.number="newVariable.max" type="number" class="form-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 可选值列表 -->
|
||||
<div class="form-group">
|
||||
<label>可选值列表(用逗号分隔)</label>
|
||||
<input v-model="newVariable.optionsText" type="text" placeholder="例如: small,medium,large"
|
||||
class="form-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button @click="closeAddVariableDialog" class="btn-cancel">取消</button>
|
||||
<button @click="saveVariable" class="btn-save" :disabled="!isValidVariable">
|
||||
{{ editingVariable ? '保存' : '添加' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导入/导出对话框 -->
|
||||
<div v-if="showImportExportDialog" class="dialog-overlay" @click="closeImportExportDialog">
|
||||
<div class="dialog large" @click.stop>
|
||||
<div class="dialog-header">
|
||||
<h4>导入/导出黑板变量</h4>
|
||||
<button @click="closeImportExportDialog" class="dialog-close">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="dialog-content">
|
||||
<div class="tab-container">
|
||||
<div class="tabs">
|
||||
<button :class="{ active: activeTab === 'export' }" @click="activeTab = 'export'">导出</button>
|
||||
<button :class="{ active: activeTab === 'import' }" @click="activeTab = 'import'">导入</button>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'export'" class="tab-content">
|
||||
<p>以下是当前黑板变量的JSON数据,可以复制保存:</p>
|
||||
<textarea v-model="exportData" readonly class="export-textarea" rows="15"></textarea>
|
||||
<button @click="copyExportData" class="btn-copy">📋 复制到剪贴板</button>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'import'" class="tab-content">
|
||||
<p>粘贴黑板变量的JSON数据进行导入:</p>
|
||||
<textarea v-model="importData" class="import-textarea" rows="15"
|
||||
placeholder="粘贴JSON数据..."></textarea>
|
||||
<div class="import-options">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="clearBeforeImport">
|
||||
<span>导入前清空现有变量</span>
|
||||
</label>
|
||||
</div>
|
||||
<button @click="importVariables" class="btn-import" :disabled="!importData.trim()">
|
||||
📥 导入变量
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,16 +0,0 @@
|
||||
<div class="tree-node-item" :style="{ paddingLeft: (level * 16) + 'px' }">
|
||||
<div class="tree-node-line" @click="$emit('node-select', node.id)">
|
||||
<span class="tree-node-icon">{{ node.icon }}</span>
|
||||
<span class="tree-node-name">{{ node.name }}</span>
|
||||
<span class="tree-node-type">({{ node.type }})</span>
|
||||
</div>
|
||||
<tree-node-item
|
||||
v-for="childId in (node.children || [])"
|
||||
:key="childId"
|
||||
:node="getNodeByIdLocal(childId)"
|
||||
:level="level + 1"
|
||||
:get-node-by-id-local="getNodeByIdLocal"
|
||||
@node-select="$emit('node-select', $event)"
|
||||
v-if="getNodeByIdLocal(childId)"
|
||||
></tree-node-item>
|
||||
</div>
|
||||
@@ -1,3 +0,0 @@
|
||||
<div id="behavior-tree-app">
|
||||
<behavior-tree-editor></behavior-tree-editor>
|
||||
</div>
|
||||
@@ -1,382 +0,0 @@
|
||||
<div class="debug-panel">
|
||||
<!-- 顶部控制栏 -->
|
||||
<div class="debug-toolbar">
|
||||
<div class="toolbar-section">
|
||||
<label>游戏实例:</label>
|
||||
<select v-model="selectedInstanceId" @change="onInstanceChanged" class="instance-selector">
|
||||
<option value="">-- 选择实例 --</option>
|
||||
<option v-for="instance in gameInstances" :key="instance.id" :value="instance.id">
|
||||
{{ instance.name }} ({{ instance.isActive ? '活跃' : '离线' }})
|
||||
</option>
|
||||
</select>
|
||||
<span class="instance-count">{{ gameInstances.length }} 个实例</span>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-section">
|
||||
<button @click="manualRefresh" class="btn-refresh" :disabled="!selectedInstanceId">
|
||||
<ui-icon value="refresh"></ui-icon>
|
||||
刷新
|
||||
</button>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="isAutoRefresh" @change="toggleAutoRefresh">
|
||||
自动刷新
|
||||
</label>
|
||||
<select v-model="refreshInterval" @change="changeRefreshInterval" :disabled="!isAutoRefresh">
|
||||
<option :value="100">0.1秒</option>
|
||||
<option :value="250">0.25秒</option>
|
||||
<option :value="500">0.5秒</option>
|
||||
<option :value="1000">1秒</option>
|
||||
<option :value="2000">2秒</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-section">
|
||||
<span class="last-update">{{ lastUpdateTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 连接状态和基础信息 -->
|
||||
<div class="debug-section">
|
||||
<div class="section-header">
|
||||
<h2>📊 实例状态</h2>
|
||||
</div>
|
||||
<div class="status-grid" v-if="selectedInstanceId && debugInfo.instanceId">
|
||||
<div class="status-item">
|
||||
<span class="label">实例名称:</span>
|
||||
<span class="value">{{ debugInfo.instanceName }}</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="label">运行状态:</span>
|
||||
<span class="value" :class="{ online: debugInfo.isRunning, offline: !debugInfo.isRunning }">
|
||||
{{ debugInfo.isRunning ? '运行中' : '已停止' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="label">当前场景:</span>
|
||||
<span class="value">{{ debugInfo.currentScene }}</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="label">运行时间:</span>
|
||||
<span class="value">{{ formatUptime(debugInfo.uptime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="no-instance" v-else>
|
||||
<ui-icon value="info" class="info-icon"></ui-icon>
|
||||
<span>请选择一个游戏实例查看详细信息</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 性能监控 -->
|
||||
<div class="debug-section" v-if="debugInfo.instanceId">
|
||||
<div class="section-header">
|
||||
<h2>⚡ 性能监控</h2>
|
||||
</div>
|
||||
<div class="performance-grid">
|
||||
<div class="perf-card">
|
||||
<div class="perf-title">帧率</div>
|
||||
<div class="perf-value" :class="getFpsColor(debugInfo.performance.fps)">
|
||||
{{ debugInfo.performance.fps }} FPS
|
||||
</div>
|
||||
<div class="perf-detail">
|
||||
引擎帧时间: {{ debugInfo.performance.engineFrameTime?.toFixed(2) || '0.00' }}ms
|
||||
</div>
|
||||
</div>
|
||||
<div class="perf-card">
|
||||
<div class="perf-title">ECS执行时间</div>
|
||||
<div class="perf-value" :class="getECSTimeColor(debugInfo.performance.ecsPercentage)">
|
||||
{{ debugInfo.performance.frameTime.toFixed(3) }}ms
|
||||
</div>
|
||||
<div class="perf-detail">
|
||||
占总帧时间: {{ debugInfo.performance.ecsPercentage?.toFixed(1) || '0.0' }}%
|
||||
</div>
|
||||
</div>
|
||||
<div class="perf-card">
|
||||
<div class="perf-title">总内存</div>
|
||||
<div class="perf-value" :class="getMemoryColor(debugInfo.memory.usedMemory / debugInfo.memory.totalMemory * 100)">
|
||||
{{ formatMemory(debugInfo.memory.usedMemory) }}
|
||||
</div>
|
||||
<div class="perf-detail">
|
||||
/ {{ formatMemory(debugInfo.memory.totalMemory) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="perf-card">
|
||||
<div class="perf-title">GC回收</div>
|
||||
<div class="perf-value">{{ debugInfo.memory.gcCollections }}</div>
|
||||
<div class="perf-detail">次数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内存分析 -->
|
||||
<div class="debug-section" v-if="debugInfo.instanceId">
|
||||
<div class="section-header">
|
||||
<h2>💾 内存分析</h2>
|
||||
</div>
|
||||
<div class="memory-breakdown">
|
||||
<div class="memory-item">
|
||||
<span class="memory-label">实体内存:</span>
|
||||
<span class="memory-value">{{ formatMemory(debugInfo.memory.entityMemory) }}</span>
|
||||
<div class="memory-bar">
|
||||
<div class="memory-fill" :style="{ width: (debugInfo.memory.entityMemory / debugInfo.memory.usedMemory * 100) + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="memory-item">
|
||||
<span class="memory-label">组件内存:</span>
|
||||
<span class="memory-value">{{ formatMemory(debugInfo.memory.componentMemory) }}</span>
|
||||
<div class="memory-bar">
|
||||
<div class="memory-fill" :style="{ width: (debugInfo.memory.componentMemory / debugInfo.memory.usedMemory * 100) + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="memory-item">
|
||||
<span class="memory-label">系统内存:</span>
|
||||
<span class="memory-value">{{ formatMemory(debugInfo.memory.systemMemory) }}</span>
|
||||
<div class="memory-bar">
|
||||
<div class="memory-fill" :style="{ width: (debugInfo.memory.systemMemory / debugInfo.memory.usedMemory * 100) + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="memory-item">
|
||||
<span class="memory-label">对象池内存:</span>
|
||||
<span class="memory-value">{{ formatMemory(debugInfo.memory.pooledMemory) }}</span>
|
||||
<div class="memory-bar">
|
||||
<div class="memory-fill" :style="{ width: (debugInfo.memory.pooledMemory / debugInfo.memory.usedMemory * 100) + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 实体统计 -->
|
||||
<div class="debug-section" v-if="debugInfo.instanceId">
|
||||
<div class="section-header">
|
||||
<h2>👥 实体统计</h2>
|
||||
</div>
|
||||
<div class="entity-stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ debugInfo.entities.total }}</div>
|
||||
<div class="stat-label">总实体数</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ debugInfo.entities.active }}</div>
|
||||
<div class="stat-label">活跃实体</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ debugInfo.entities.inactive }}</div>
|
||||
<div class="stat-label">非活跃实体</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{{ debugInfo.entities.pendingAdd }}</div>
|
||||
<div class="stat-label">待添加</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Archetype分布 -->
|
||||
<div class="archetype-section" v-if="debugInfo.entities.entitiesPerArchetype.length > 0">
|
||||
<h3>Archetype 分布</h3>
|
||||
<div class="archetype-list">
|
||||
<div v-for="archetype in debugInfo.entities.entitiesPerArchetype" :key="archetype.signature" class="archetype-item">
|
||||
<div class="archetype-signature">{{ archetype.signature }}</div>
|
||||
<div class="archetype-stats">
|
||||
<span class="entity-count">{{ archetype.count }} 实体</span>
|
||||
<span class="memory-usage">{{ formatMemory(archetype.memory) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 组件分析 -->
|
||||
<div class="debug-section" v-if="debugInfo.instanceId">
|
||||
<div class="section-header">
|
||||
<h2>🧩 组件分析</h2>
|
||||
<div class="section-help">
|
||||
<button class="help-btn" @click="showComponentPoolHelp = !showComponentPoolHelp" title="组件对象池使用说明">
|
||||
<ui-icon value="help"></ui-icon>
|
||||
对象池使用说明
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 对象池使用说明 -->
|
||||
<div v-if="showComponentPoolHelp" class="help-panel">
|
||||
<div class="help-content">
|
||||
<h4>📖 组件对象池使用指南</h4>
|
||||
<p>组件对象池用于复用组件实例,减少频繁创建/销毁组件带来的内存分配开销。当前利用率为0表示还未配置对象池。</p>
|
||||
|
||||
<div class="help-steps">
|
||||
<div class="help-step">
|
||||
<h5>1. 注册组件对象池</h5>
|
||||
<pre class="code-example">import { ComponentPoolManager } from '@esengine/ecs-framework';
|
||||
|
||||
// 为Transform组件注册对象池
|
||||
ComponentPoolManager.getInstance().registerPool(
|
||||
'Transform',
|
||||
() => new Transform(),
|
||||
(comp) => comp.reset(), // 可选的重置函数
|
||||
100 // 池大小
|
||||
);</pre>
|
||||
</div>
|
||||
|
||||
<div class="help-step">
|
||||
<h5>2. 使用对象池获取组件</h5>
|
||||
<pre class="code-example">// 从对象池获取组件实例
|
||||
const poolManager = ComponentPoolManager.getInstance();
|
||||
const transform = poolManager.acquireComponent('Transform');
|
||||
|
||||
// 使用完毕后释放回池中
|
||||
poolManager.releaseComponent('Transform', transform);</pre>
|
||||
</div>
|
||||
|
||||
<div class="help-step">
|
||||
<h5>3. 查看性能改进</h5>
|
||||
<p>正确配置后,利用率栏将显示池的使用情况。利用率越高说明对象池被有效使用,可以减少GC压力。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-links">
|
||||
<a href="#" @click="openDocumentation('component-pool')" class="doc-link">
|
||||
<ui-icon value="book"></ui-icon>
|
||||
查看详细文档
|
||||
</a>
|
||||
<a href="#" @click="openDocumentation('performance-optimization')" class="doc-link">
|
||||
<ui-icon value="speed"></ui-icon>
|
||||
性能优化指南
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="doc-access-note">
|
||||
<ui-icon value="info"></ui-icon>
|
||||
<span>如果无法直接打开文档链接,链接将自动复制到剪贴板,请在浏览器中粘贴访问</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="component-overview">
|
||||
<div class="overview-item">
|
||||
<span class="label">组件类型:</span>
|
||||
<span class="value">{{ debugInfo.components.totalTypes }}</span>
|
||||
</div>
|
||||
<div class="overview-item">
|
||||
<span class="label">组件实例:</span>
|
||||
<span class="value">{{ debugInfo.components.totalInstances }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="component-table" v-if="debugInfo.components.componentStats.length > 0">
|
||||
<div class="table-header">
|
||||
<div class="col-name">组件类型</div>
|
||||
<div class="col-count">实例数</div>
|
||||
<div class="col-memory">内存占用</div>
|
||||
<div class="col-pool">对象池</div>
|
||||
<div class="col-efficiency">
|
||||
利用率
|
||||
<span class="help-tooltip" title="对象池利用率:使用中的实例 / 池总大小。利用率为0表示未配置对象池">
|
||||
<ui-icon value="help"></ui-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="comp in debugInfo.components.componentStats" :key="comp.typeName" class="table-row">
|
||||
<div class="col-name">{{ comp.typeName }}</div>
|
||||
<div class="col-count">{{ comp.instanceCount }}</div>
|
||||
<div class="col-memory">{{ formatMemory(comp.totalMemory) }}</div>
|
||||
<div class="col-pool">
|
||||
{{ comp.poolSize || '未配置' }}
|
||||
<span v-if="comp.poolSize === 0" class="not-configured" title="点击上方说明了解如何配置对象池">
|
||||
<ui-icon value="warning"></ui-icon>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-efficiency">
|
||||
<div class="efficiency-bar">
|
||||
<div class="efficiency-fill" :style="{ width: comp.poolUtilization + '%' }"></div>
|
||||
</div>
|
||||
{{ comp.poolUtilization.toFixed(1) }}%
|
||||
<span v-if="comp.poolUtilization === 0 && comp.poolSize === 0" class="no-pool-hint">
|
||||
(无对象池)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 无对象池组件提示 -->
|
||||
<div v-if="debugInfo.components.componentStats.length > 0 && debugInfo.components.componentStats.every(c => c.poolSize === 0)" class="pool-suggestion">
|
||||
<div class="suggestion-content">
|
||||
<ui-icon value="lightbulb" class="suggestion-icon"></ui-icon>
|
||||
<div class="suggestion-text">
|
||||
<h4>💡 性能优化建议</h4>
|
||||
<p>检测到所有组件都未配置对象池。为频繁创建/销毁的组件配置对象池可以显著提升性能,减少垃圾回收压力。</p>
|
||||
<button @click="showComponentPoolHelp = true" class="suggestion-action">
|
||||
了解如何配置对象池
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统性能 -->
|
||||
<div class="debug-section" v-if="debugInfo.instanceId">
|
||||
<div class="section-header">
|
||||
<h2>⚙️ 系统性能</h2>
|
||||
</div>
|
||||
<div class="systems-table" v-if="debugInfo.systems.systemStats.length > 0">
|
||||
<div class="table-header">
|
||||
<div class="col-name">系统名称</div>
|
||||
<div class="col-entities">实体数</div>
|
||||
<div class="col-time">执行时间</div>
|
||||
<div class="col-percentage">ECS占比</div>
|
||||
<div class="col-order">优先级</div>
|
||||
<div class="col-status">状态</div>
|
||||
</div>
|
||||
<div v-for="system in debugInfo.systems.systemStats" :key="system.name" class="table-row">
|
||||
<div class="col-name">{{ system.name }}</div>
|
||||
<div class="col-entities">{{ system.entityCount }}</div>
|
||||
<div class="col-time">
|
||||
<span :class="getExecutionTimeColor(system.averageExecutionTime)">
|
||||
{{ system.averageExecutionTime.toFixed(3) }}ms
|
||||
</span>
|
||||
<div class="time-range">
|
||||
({{ system.minExecutionTime.toFixed(3) }}-{{ system.maxExecutionTime.toFixed(3) }}ms)
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-percentage">
|
||||
<div class="percentage-bar">
|
||||
<div class="percentage-fill" :style="{ width: (system.percentage || 0) + '%' }"></div>
|
||||
</div>
|
||||
{{ (system.percentage || 0).toFixed(1) }}%
|
||||
</div>
|
||||
<div class="col-order">{{ system.updateOrder }}</div>
|
||||
<div class="col-status">
|
||||
<span :class="{ enabled: system.enabled, disabled: !system.enabled }">
|
||||
{{ system.enabled ? '启用' : '禁用' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务器状态 -->
|
||||
<div class="debug-section" v-if="gameInstances.length === 0">
|
||||
<div class="section-header">
|
||||
<h2>🔌 调试服务器</h2>
|
||||
</div>
|
||||
<div class="server-status">
|
||||
<div class="status-message">
|
||||
<ui-icon value="wifi" class="server-icon"></ui-icon>
|
||||
<div class="message-content">
|
||||
<h3>等待游戏实例连接...</h3>
|
||||
<p>调试服务器正在 <strong>ws://localhost:8080</strong> 监听连接</p>
|
||||
<p>请确保游戏中的ECS框架已启用调试模式</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="connection-help">
|
||||
<h4>连接说明:</h4>
|
||||
<ul>
|
||||
<li>在ECSManager组件中启用调试模式</li>
|
||||
<li>运行游戏,框架会自动连接到调试服务器</li>
|
||||
<li>支持多个游戏实例同时连接</li>
|
||||
<li>可以在上方选择不同的实例进行调试</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +0,0 @@
|
||||
<div class="ecs-welcome-panel">
|
||||
<div id="app">
|
||||
<ecs-welcome></ecs-welcome>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,111 +0,0 @@
|
||||
<div class="ecs-generator">
|
||||
<div class="generator-container">
|
||||
<!-- 头部区域 -->
|
||||
<div class="generator-header">
|
||||
<h1>🛠️ ECS 代码生成器</h1>
|
||||
<p>输入功能名称,快速生成组件和系统代码</p>
|
||||
</div>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<div class="generator-form">
|
||||
<div class="form-title">📝 基础设置</div>
|
||||
|
||||
<!-- 功能名称输入 -->
|
||||
<div class="input-group">
|
||||
<label class="input-label">功能名称</label>
|
||||
<input
|
||||
v-model="featureName"
|
||||
@input="updatePreview"
|
||||
type="text"
|
||||
class="feature-input"
|
||||
placeholder="例如:Health、Movement、Combat"
|
||||
maxlength="50"
|
||||
/>
|
||||
<div class="input-hint">
|
||||
💡 使用英文名称,首字母大写,例如:Health、Movement、Combat
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 生成选项 -->
|
||||
<div class="options-group">
|
||||
<label class="input-label">生成内容</label>
|
||||
|
||||
<div class="option-item">
|
||||
<label class="checkbox-wrapper">
|
||||
<input type="checkbox" v-model="options.generateComponent" @change="updatePreview">
|
||||
<span class="option-title">📦 生成组件 (Component)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-description" v-if="options.generateComponent">
|
||||
创建基础组件类,包含reset()方法和基本结构
|
||||
</div>
|
||||
|
||||
<div class="option-item">
|
||||
<label class="checkbox-wrapper">
|
||||
<input type="checkbox" v-model="options.generateSystem" @change="updatePreview">
|
||||
<span class="option-title">⚙️ 生成系统 (System)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-description" v-if="options.generateSystem">
|
||||
创建处理组件的系统类,包含完整的生命周期方法
|
||||
</div>
|
||||
|
||||
<!-- 系统详细配置 -->
|
||||
<div class="system-config" v-if="options.generateSystem">
|
||||
<!-- 组件过滤选项 -->
|
||||
<div class="config-section">
|
||||
<label class="config-label">🔍 组件过滤</label>
|
||||
<div class="filter-option">
|
||||
<label class="checkbox-wrapper">
|
||||
<input type="checkbox" v-model="systemOptions.filterByComponent" @change="updatePreview">
|
||||
<span class="option-title">过滤包含组件的实体</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="option-description" v-if="systemOptions.filterByComponent">
|
||||
系统只处理包含指定组件的实体。如果勾选了"生成组件",会自动过滤{{featureName}}Component
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统类型选择 -->
|
||||
<div class="config-section">
|
||||
<label class="config-label">🎯 系统类型</label>
|
||||
<div class="system-type-cards">
|
||||
<div
|
||||
v-for="type in systemTypes"
|
||||
:key="type.value"
|
||||
class="system-card"
|
||||
:class="{ active: systemOptions.systemType === type.value }"
|
||||
@click="selectSystemType(type.value)">
|
||||
<div class="card-header">
|
||||
<span class="card-icon">{{ type.icon }}</span>
|
||||
<span class="card-title">{{ type.name }}</span>
|
||||
</div>
|
||||
<div class="card-description">{{ type.description }}</div>
|
||||
<div class="card-usage">{{ type.usage }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预览区域 -->
|
||||
<div class="preview-section" v-if="showPreview">
|
||||
<div class="preview-title">👀 生成预览</div>
|
||||
<div class="preview-content">{{ previewCode }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 生成按钮 -->
|
||||
<div class="action-section">
|
||||
<button
|
||||
class="generate-btn"
|
||||
:disabled="isGenerating || (!options.generateComponent && !options.generateSystem) || !featureName.trim()"
|
||||
@click="generateCode">
|
||||
<span class="btn-icon" :class="{ spinning: isGenerating }">
|
||||
{{ isGenerating ? '⏳' : '🚀' }}
|
||||
</span>
|
||||
{{ isGenerating ? '生成中...' : '生成代码' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +0,0 @@
|
||||
<div class="counter">
|
||||
<h2> {{counter}}</h2>
|
||||
<ui-button class="blue"
|
||||
@click="addition">+</ui-button>
|
||||
<ui-button @click="subtraction">-</ui-button>
|
||||
</div>
|
||||
@@ -1,162 +0,0 @@
|
||||
<div class="welcome-container">
|
||||
<!-- 头部欢迎区域 -->
|
||||
<div class="header">
|
||||
<h1>🎮 ECS Framework for Cocos Creator</h1>
|
||||
<p>高性能实体组件系统框架</p>
|
||||
</div>
|
||||
|
||||
<!-- 操作状态显示区域 -->
|
||||
<div class="status-section" v-if="showOperationStatus">
|
||||
<div class="status-title">📋 操作状态</div>
|
||||
<div class="operation-status" :class="operationStatusType">
|
||||
<div class="status-content">
|
||||
<span v-if="operationStatusType === 'loading'" class="loading-spinner"></span>
|
||||
<span class="status-icon">{{ getStatusIcon(operationStatusType) }}</span>
|
||||
<span class="status-text">{{ operationStatusMessage }}</span>
|
||||
</div>
|
||||
<div v-if="operationStatusDetails" class="status-details">
|
||||
{{ operationStatusDetails }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 安装状态检测区域 -->
|
||||
<div class="status-section">
|
||||
<div class="status-title">📦 安装状态检测</div>
|
||||
|
||||
<div class="status-item">
|
||||
<span class="status-label">ECS Framework (@esengine/ecs-framework)</span>
|
||||
<div v-if="checkingStatus" class="status-badge checking">
|
||||
<span class="loading-spinner"></span>检测中...
|
||||
</div>
|
||||
<div v-else-if="ecsInstalled" class="status-badge installed">
|
||||
✅ 已安装 <span class="version-info">v{{ecsVersion}}</span>
|
||||
<span v-if="hasUpdate" class="update-hint">有新版本:v{{latestVersion}}</span>
|
||||
</div>
|
||||
<div v-else class="status-badge not-installed">
|
||||
❌ 未安装
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-item">
|
||||
<span class="status-label">项目package.json</span>
|
||||
<div v-if="packageJsonExists" class="status-badge installed">
|
||||
✅ 存在
|
||||
</div>
|
||||
<div v-else class="status-badge not-installed">
|
||||
❌ 不存在
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-item">
|
||||
<span class="status-label">Node.js环境</span>
|
||||
<div class="status-badge installed">
|
||||
✅ v{{nodeVersion}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ECS框架管理区域 -->
|
||||
<div class="status-section" v-if="ecsInstalled">
|
||||
<div class="status-title">🔧 ECS Framework 管理</div>
|
||||
|
||||
<div class="management-buttons">
|
||||
<ui-button
|
||||
class="green"
|
||||
:disabled="!hasUpdate || updating"
|
||||
@click="updateEcsFramework">
|
||||
<span v-if="updating" class="loading-spinner"></span>
|
||||
{{ updating ? '更新中...' : (hasUpdate ? `更新到 v${latestVersion}` : '已是最新版本') }}
|
||||
</ui-button>
|
||||
|
||||
<ui-button
|
||||
class="red"
|
||||
:disabled="uninstalling"
|
||||
@click="uninstallEcsFramework">
|
||||
<span v-if="uninstalling" class="loading-spinner"></span>
|
||||
{{ uninstalling ? '卸载中...' : '卸载 ECS Framework' }}
|
||||
</ui-button>
|
||||
|
||||
<ui-button
|
||||
class="blue"
|
||||
@click="checkForUpdates">
|
||||
🔄 检查更新
|
||||
</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快速操作区域 -->
|
||||
<div class="actions-section">
|
||||
<div class="status-title">🚀 快速开始</div>
|
||||
|
||||
<div class="action-card" :class="{ disabled: !packageJsonExists || installing }" @click="installEcsFramework">
|
||||
<h3>
|
||||
<span class="icon">📥</span>
|
||||
{{ ecsInstalled ? '重新安装 ECS Framework' : '安装 ECS Framework' }}
|
||||
</h3>
|
||||
<p v-if="installing">正在安装中,请稍候...</p>
|
||||
<p v-else-if="!packageJsonExists">请先确保项目有package.json文件</p>
|
||||
<p v-else-if="ecsInstalled">重新安装当前版本的ECS框架</p>
|
||||
<p v-else>安装@esengine/ecs-framework到当前项目</p>
|
||||
</div>
|
||||
|
||||
<div class="action-card" @click="openDocumentation">
|
||||
<h3>
|
||||
<span class="icon">📚</span>
|
||||
查看文档
|
||||
</h3>
|
||||
<p>打开ECS框架完整使用文档和教程</p>
|
||||
</div>
|
||||
|
||||
<div class="action-card" :class="{ disabled: !ecsInstalled || templateExists }" @click="createEcsTemplate">
|
||||
<h3>
|
||||
<span class="icon">🛠️</span>
|
||||
创建ECS模板
|
||||
</h3>
|
||||
<p v-if="!ecsInstalled">请先安装 ECS Framework</p>
|
||||
<p v-else-if="templateExists">⚠️ ECS模板已存在,避免覆盖现有代码</p>
|
||||
<p v-else>生成基础的ECS项目结构和启动代码</p>
|
||||
|
||||
<div v-if="templateExists && existingFiles.length > 0" class="existing-files-info">
|
||||
<div class="files-summary">
|
||||
检测到 {{existingFiles.length}} 个已存在的文件
|
||||
</div>
|
||||
<div class="files-list" v-if="existingFiles.length <= 10">
|
||||
<div v-for="file in existingFiles" :key="file" class="file-item">
|
||||
📄 {{file}}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="files-overflow">
|
||||
<div v-for="file in existingFiles.slice(0, 8)" :key="file" class="file-item">
|
||||
📄 {{file}}
|
||||
</div>
|
||||
<div class="more-files">... 还有 {{existingFiles.length - 8}} 个文件</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 代码生成工具区域 -->
|
||||
<div class="actions-section" v-if="ecsInstalled">
|
||||
<div class="status-title">🛠️ 代码生成工具</div>
|
||||
|
||||
<div class="action-card special" @click="openGenerator">
|
||||
<h3>
|
||||
<span class="icon">🚀</span>
|
||||
代码生成器
|
||||
</h3>
|
||||
<p>一站式代码生成工具,输入功能名称快速生成组件和系统代码</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<div class="footer">
|
||||
<p>ECS Framework Plugin v{{pluginVersion}} | 基于Vue 3.x构建</p>
|
||||
<p>
|
||||
更多信息请访问: <a href="#" @click="openGithub">GitHub Repository</a> |
|
||||
<a href="#" @click="joinQQGroup">💬 加入QQ群交流</a>
|
||||
</p>
|
||||
<p v-if="lastCheckTime" class="last-check">最后检查时间: {{lastCheckTime}}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,132 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 模拟项目路径(实际会是真实的项目路径)
|
||||
const projectPath = process.cwd();
|
||||
const settingsPath = path.join(projectPath, '.ecs-framework-settings.json');
|
||||
|
||||
console.log('🧪 测试ECS框架设置功能...');
|
||||
console.log('设置文件路径:', settingsPath);
|
||||
|
||||
// 默认设置
|
||||
const testSettings = {
|
||||
codeGeneration: {
|
||||
template: 'typescript',
|
||||
useStrictMode: true,
|
||||
generateComments: true,
|
||||
generateImports: true,
|
||||
componentSuffix: 'Component',
|
||||
systemSuffix: 'System',
|
||||
indentStyle: 'spaces',
|
||||
indentSize: 4
|
||||
},
|
||||
performance: {
|
||||
enableMonitoring: true,
|
||||
warningThreshold: 16.67,
|
||||
criticalThreshold: 33.33,
|
||||
memoryWarningMB: 100,
|
||||
memoryCriticalMB: 200,
|
||||
maxRecentSamples: 60,
|
||||
enableFpsMonitoring: true,
|
||||
targetFps: 60
|
||||
},
|
||||
debugging: {
|
||||
enableDebugMode: true,
|
||||
showEntityCount: true,
|
||||
showSystemExecutionTime: true,
|
||||
enablePerformanceWarnings: true,
|
||||
logLevel: 'info',
|
||||
enableDetailedLogs: false
|
||||
},
|
||||
editor: {
|
||||
autoRefreshAssets: true,
|
||||
showWelcomePanelOnStartup: true,
|
||||
enableAutoUpdates: false,
|
||||
updateChannel: 'stable',
|
||||
enableNotifications: true
|
||||
},
|
||||
template: {
|
||||
defaultEntityName: 'TestEntity', // 修改这个值来测试
|
||||
defaultComponentName: 'TestComponent',
|
||||
defaultSystemName: 'TestSystem',
|
||||
createExampleFiles: true,
|
||||
includeDocumentation: true,
|
||||
useFactoryPattern: true
|
||||
},
|
||||
events: {
|
||||
enableEventSystem: true,
|
||||
defaultEventPriority: 0,
|
||||
enableAsyncEvents: true,
|
||||
enableEventBatching: false,
|
||||
batchSize: 10,
|
||||
batchDelay: 16,
|
||||
maxEventListeners: 100
|
||||
}
|
||||
};
|
||||
|
||||
// 测试保存功能
|
||||
console.log('✅ 测试保存设置...');
|
||||
try {
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(testSettings, null, 2), 'utf-8');
|
||||
console.log('✅ 设置已成功保存到:', settingsPath);
|
||||
} catch (error) {
|
||||
console.error('❌ 保存设置失败:', error);
|
||||
}
|
||||
|
||||
// 测试加载功能
|
||||
console.log('✅ 测试加载设置...');
|
||||
try {
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
const loadedData = fs.readFileSync(settingsPath, 'utf-8');
|
||||
const loadedSettings = JSON.parse(loadedData);
|
||||
|
||||
console.log('✅ 设置已成功加载');
|
||||
console.log('默认实体名称:', loadedSettings.template.defaultEntityName);
|
||||
console.log('调试模式:', loadedSettings.debugging.enableDebugMode);
|
||||
console.log('目标FPS:', loadedSettings.performance.targetFps);
|
||||
|
||||
// 验证数据完整性
|
||||
const expectedKeys = Object.keys(testSettings);
|
||||
const loadedKeys = Object.keys(loadedSettings);
|
||||
|
||||
if (expectedKeys.every(key => loadedKeys.includes(key))) {
|
||||
console.log('✅ 数据完整性检查通过');
|
||||
} else {
|
||||
console.log('❌ 数据完整性检查失败');
|
||||
}
|
||||
} else {
|
||||
console.log('❌ 设置文件不存在');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 加载设置失败:', error);
|
||||
}
|
||||
|
||||
// 测试修改和重新保存
|
||||
console.log('✅ 测试修改设置...');
|
||||
try {
|
||||
const modifiedSettings = { ...testSettings };
|
||||
modifiedSettings.template.defaultEntityName = 'ModifiedEntity';
|
||||
modifiedSettings.performance.targetFps = 120;
|
||||
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(modifiedSettings, null, 2), 'utf-8');
|
||||
|
||||
// 重新加载验证
|
||||
const reloadedData = fs.readFileSync(settingsPath, 'utf-8');
|
||||
const reloadedSettings = JSON.parse(reloadedData);
|
||||
|
||||
if (reloadedSettings.template.defaultEntityName === 'ModifiedEntity' &&
|
||||
reloadedSettings.performance.targetFps === 120) {
|
||||
console.log('✅ 设置修改测试通过');
|
||||
} else {
|
||||
console.log('❌ 设置修改测试失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 修改设置测试失败:', error);
|
||||
}
|
||||
|
||||
console.log('🎉 测试完成!设置功能工作正常。');
|
||||
console.log('📁 设置文件位置:', settingsPath);
|
||||
|
||||
// 清理测试文件(可选)
|
||||
// fs.unlinkSync(settingsPath);
|
||||
// console.log('🧹 已清理测试文件');
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"extends": "./base.tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./source",
|
||||
"types": [
|
||||
"node",
|
||||
"@cocos/creator-types/editor",
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"admin-backend/**/*",
|
||||
"admin-desktop/**/*"
|
||||
]
|
||||
}
|
||||
1
thirdparty/admin-backend
vendored
Submodule
1
thirdparty/admin-backend
vendored
Submodule
Submodule thirdparty/admin-backend added at 897eb02011
Reference in New Issue
Block a user