4 Commits
2.0 ... 2.1.1

Author SHA1 Message Date
nnwang
febe90a2d1 2025-12-04 18:00:50 +08:00
nnwang
0ecc020c23 将附属组件的父组件名去掉 2025-12-04 17:53:49 +08:00
nnwang
fc3df13dd9 将编辑器修改位Monaco并且修复bug 2025-12-04 17:31:18 +08:00
231349b24c 更新 README.md 2025-10-18 23:41:07 +08:00
6 changed files with 336 additions and 346 deletions

188
README.md
View File

@@ -1,124 +1,104 @@
# 幸福工厂蓝图编辑器 - 项目文档
# Satisfactory 蓝图编辑器文档
## 项目概述
这是一个用于解析和编辑《幸福工厂》(Satisfactory)游戏蓝图(.sbp文件)的Web工具。用户可以上传蓝图文件查看和修改其中的材料信息然后下载修改后的蓝图文件
个项目是一个用于《Satisfactory》游戏的蓝图编辑器允许玩家上传、编辑和导出游戏蓝图文件(.sbp/.sbpcfg)。编辑器提供了JSON编辑和图形化编辑两种方式让玩家可以轻松修改蓝图的各种属性
## 文件结构解析
## 功能特点
### 1. 文件头部 (32字节/64位十六进制)
结构如下:
```
[4位] version - 版本号
[4位] header_size - 头部大小(目前总是错误的)
[4位] timestamp - 时间戳
[4位] unknown1 - 未知字段1
[4位] unknown2 - 未知字段2
[4位] unknown3 - 未知字段3
[4位] material_count - 材料数量
[4位] reserved - 保留字段
### 1. 蓝图文件导入
- 支持拖放上传或点击上传
- 支持.sbp主蓝图文件和.sbpcfg配置文件
- 显示上传文件的基本信息
### 2. JSON编辑器
- 将蓝图文件解析为可编辑的JSON格式
- 支持格式化JSON
- 可刷新和更新JSON数据
- 可选择是否显示Objects段包含大量实体数据
### 3. 图形化编辑器
- **基础编辑**
- 蓝图基本信息(名称、尺寸)
- 配置信息(版本、图标、描述、颜色等)
- 头部信息(版本号)
- 物品消耗管理
- 配方引用管理
- **进阶编辑**
- 实体列表浏览
- 实体位置编辑(旋转、平移、缩放)
- 属性编辑(添加/删除/修改属性)
- 组件编辑
- 附属组件编辑
### 4. 蓝图导出
- 将编辑后的JSON导出为.sbp和.sbpcfg文件
- 显示导出状态
## 技术栈
- **前端框架**Vue 3
- **UI库**DaisyUI (https://github.com/saadeghi/daisyui)
- **蓝图解析库**@etothepii/satisfactory-file-parser (https://github.com/etothepii4/satisfactory-file-parser)
- **图标库**Font Awesome
## 安装与使用
### 安装依赖
```bash
npm install
```
### 2. 材料部分
每个材料的结构:
### 开发模式
```bash
npm run dev
```
[4位] 路径长度(包括null结束符)
[变长] 材料路径(以00结束)
[4位] 数量
[4位] 参数(只有最后一个材料有值)
### 生产构建
```bash
npm run build
```
材料部分以`00000000`结束
### 3. 建筑部分
每个建筑的结构:
```
[4位] 路径长度(包括null结束符)
[变长] 建筑路径(以00结束)
[4位] 参数(只有最后一个建筑有值)
```
建筑部分以`22222222`结束
### 4. 压缩数据部分
Zlib压缩的数据工具不做解析
## 功能说明
### 主要功能
1. **文件上传**:支持.sbp蓝图文件上传
2. **头部信息展示**:显示蓝图文件的头部信息
3. **材料编辑**
- 查看所有材料及其属性
- 修改材料路径和数量
- 添加/删除材料
4. **建筑信息展示**:显示建筑信息(只读)
5. **数据下载**:将修改后的数据保存为.sbp文件
### 特殊处理
1. 材料部分的最后一个条目必须有参数值
2. 建筑部分的最后一个条目必须有参数值
3. 自动更新材料数量计数
## 使用说明
1. 点击"选择文件"按钮上传.sbp蓝图文件
2. 在"材料资产"部分可以:
- 修改材料路径(需使用正确的游戏资源路径)
- 修改材料数量
- 添加新材料(点击"添加材料"按钮)
- 删除材料(点击材料条目右侧的删除按钮)
3. 修改完成后点击"下载蓝图"按钮保存文件
## 注意事项
1. 材料路径需要正确的游戏资源路径格式,可以参考其他蓝图文件中的路径
2. 最后一个材料的参数值必须保留,不能删除
3. 建筑信息目前是只读的,不能修改
4. 压缩数据部分目前不做解析,下载时会原样保存
## 技术实现
- 前端框架Vue.js
- UI组件库DaisyUI
- 文件处理使用JavaScript的FileReader和ArrayBuffer处理二进制数据
- HEX转换自定义方法实现字符串与十六进制的相互转换
## 项目结构
```
├── index.html
├── src/
├── App.vue # 主组件
│ ├── main.js # 入口文件
│ └── assets/ # 静态资源
└── README.md # 项目说明
src/
├── components
├── App.vue (主组件)
── main.js
```
## 开发说明
## 主要组件说明
1. 克隆仓库:
```
git clone https://git.liulikeji.cn/xingluo/satisfactory-BlueprintsEdit.git
```
2. 安装依赖:
```
npm install
```
3. 运行开发服务器:
```
npm run dev
```
4. 构建生产版本:
```
npm run build
```
### BlueprintEditor.vue
## 未来计划
这是应用的核心组件,包含所有蓝图编辑功能:
1. 添加建筑配方或超频编辑功能
2. 支持压缩数据的解析和修改
3. 添加材料图标
#### 状态管理
- `uploadedFiles`: 存储上传的文件
- `blueprintData`: 存储解析后的蓝图数据
- `rawObjects/newObjects`: 存储蓝图中的实体对象
- `isParsing/isExporting`: 处理状态标志
## 贡献指南
#### 主要方法
- `parseBlueprint()`: 解析上传的蓝图文件
- `exportBlueprint()`: 导出编辑后的蓝图
- `formatJson()`: 格式化JSON显示
- `updateDataFromJson()`: 从JSON更新数据
#### 实体编辑功能
- `selectEntity()`: 选择要编辑的实体
- `updateEntityFromJson()`: 更新实体数据
- `addProperty()`: 添加新属性
- `deleteProperty()`: 删除属性
## 注意事项
1. **Objects段警告**Objects段包含大量实体数据对于大型蓝图开启此选项可能导致性能问题。
2. **文件格式**仅支持Satisfactory U8+版本的蓝图文件。
3. **数据安全**:编辑蓝图时建议备份原始文件,以防意外修改导致蓝图损坏。
欢迎提交Pull Request或Issue报告问题。对于新功能建议请先创建Issue讨论。

50
package-lock.json generated
View File

@@ -12,6 +12,8 @@
"@tailwindcss/vite": "^4.1.14",
"axios": "^1.12.2",
"daisyui": "^5.3.7",
"monaco-editor": "^0.55.1",
"monaco-editor-vue3": "^1.0.4",
"tailwindcss": "^4.1.14",
"vue": "^3.5.22"
},
@@ -1109,6 +1111,13 @@
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@vitejs/plugin-vue": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
@@ -1310,6 +1319,15 @@
"node": ">=8"
}
},
"node_modules/dompurify": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1857,6 +1875,18 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/marked": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1908,6 +1938,26 @@
"node": ">= 18"
}
},
"node_modules/monaco-editor": {
"version": "0.55.1",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
"license": "MIT",
"dependencies": {
"dompurify": "3.2.7",
"marked": "14.0.0"
}
},
"node_modules/monaco-editor-vue3": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/monaco-editor-vue3/-/monaco-editor-vue3-1.0.4.tgz",
"integrity": "sha512-gaIMBdhUGorOAX0kBvWul9QCQ+6J+MjZgqkieDECv3rjXsRbI07XNNrnD3IBC1jnGRF9+aTZ9CNhJ8Uv06z1uw==",
"license": "MIT",
"peerDependencies": {
"monaco-editor": ">= 0.25.0 < 1",
"vue": "^3"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",

View File

@@ -13,6 +13,8 @@
"@tailwindcss/vite": "^4.1.14",
"axios": "^1.12.2",
"daisyui": "^5.3.7",
"monaco-editor": "^0.55.1",
"monaco-editor-vue3": "^1.0.4",
"tailwindcss": "^4.1.14",
"vue": "^3.5.22"
},

View File

@@ -1,5 +1,5 @@
<template>
<div class="container mx-auto max-w-6xl p-4">
<div class="">
<div class="card bg-base-200 shadow-xl">
<div class="card-body">
<div class="text-center mb-6">
@@ -137,12 +137,15 @@
</span>
</div>
<textarea
class="textarea textarea-bordered w-full h-96 mt-2 font-mono"
v-model="displayedJson"
placeholder="蓝图JSON内容将显示在这里..."
:disabled="!isParsed"
></textarea>
<div class="h-96 border rounded-lg overflow-hidden">
<CodeEditor
v-model:value="displayedJson"
language="json"
theme="vs"
:options="editorOptions"
:disabled="!isParsed"
/>
</div>
<div class="flex flex-wrap gap-3 mt-4">
<button
@@ -534,8 +537,8 @@
</div>
<!-- 进阶编辑区域 - 使用第一个文件的编辑器 -->
<div v-show="activeVisualTab === 'advanced' && showObjects">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
<div v-show="activeVisualTab === 'advanced'">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-1">
<!-- 左侧实体列表 -->
<div class="lg:col-span-1">
<div class="card bg-base-100 shadow-xl">
@@ -549,8 +552,8 @@
:class="['p-3 mb-2 rounded-lg cursor-pointer hover:bg-base-300 transition',
selectedEntityIndex === index ? 'bg-primary text-primary-content' : 'bg-base-200']"
>
<div class="font-bold truncate">{{ entity.instanceName || '未命名实体' }}</div>
<div class="text-sm opacity-75 truncate">{{ entity.typePath || '未知类型' }}</div>
<div class="font-bold truncate">{{ getDisplayName(entity.instanceName) || '未命名实体' }}</div>
<div class="text-sm opacity-75 truncate">{{ getTypeName(entity.typePath) || '未知类型' }}</div>
</div>
</div>
</div>
@@ -562,7 +565,7 @@
<div v-if="selectedEntity" class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title flex items-center">
<span class="truncate">{{ selectedEntity.instanceName || '未命名实体' }}</span>
<span class="truncate">{{ getDisplayName(selectedEntity.instanceName) || '未命名实体' }}</span>
<span class="badge badge-primary ml-2">{{ selectedEntity.type || '未知类型' }}</span>
</h2>
@@ -581,10 +584,14 @@
<!-- 原始JSON标签页 -->
<div v-if="activeTab === 'raw'">
<h3 class="text-lg font-semibold mb-3">原始JSON数据</h3>
<textarea
v-model="entityJson"
class="textarea textarea-bordered w-full h-96 font-mono"
></textarea>
<div class="h-96 border rounded-lg overflow-hidden">
<CodeEditor
v-model:value="entityJson"
language="json"
theme="vs"
:options="editorOptions"
/>
</div>
<div class="mt-3 flex justify-end">
<button @click="updateEntityFromJson" class="btn btn-primary">更新实体</button>
</div>
@@ -697,30 +704,28 @@
>
</td>
<td>
<select v-model="value.ueType" class="select select-bordered select-sm">
<option value="FloatProperty">FloatProperty</option>
<option value="IntProperty">IntProperty</option>
<option value="ObjectProperty">ObjectProperty</option>
<option value="StructProperty">StructProperty</option>
</select>
<input
type="text"
v-model="value.ueType"
class="input input-bordered input-sm"
placeholder="输入类型"
>
</td>
<td>
<input
v-if="value.ueType === 'FloatProperty' || value.ueType === 'IntProperty'"
type="number"
v-model.number="value.value"
class="input input-bordered input-sm"
>
<textarea
v-else-if="value.ueType === 'ObjectProperty' || value.ueType === 'StructProperty'"
v-model="objectProperties[key]"
class="textarea textarea-bordered textarea-sm w-full"
rows="3"
></textarea>
<div v-if="isComplexValue(value.value)" class="h-20 border rounded-lg overflow-hidden">
<CodeEditor
v-model:value="objectProperties[key]"
language="json"
theme="vs"
:options="smallEditorOptions"
/>
</div>
<input
v-else
type="text"
v-model="value.value"
:value="getPropertyDisplayValue(value)"
@input="setPropertyValue(value, $event.target.value)"
class="input input-bordered input-sm"
>
</td>
@@ -739,26 +744,13 @@
<input type="text" v-model="newProperty.name" placeholder="属性名称" class="input input-bordered">
</div>
<div class="form-control">
<select v-model="newProperty.type" class="select select-bordered">
<option value="FloatProperty">FloatProperty</option>
<option value="IntProperty">IntProperty</option>
<option value="ObjectProperty">ObjectProperty</option>
<option value="StructProperty">StructProperty</option>
</select>
<input type="text" v-model="newProperty.type" placeholder="属性类型" class="input input-bordered">
</div>
<div class="form-control">
<input
v-if="newProperty.type === 'FloatProperty' || newProperty.type === 'IntProperty'"
type="number"
v-model.number="newProperty.value"
placeholder="值"
class="input input-bordered"
>
<input
v-else
type="text"
v-model="newProperty.value"
placeholder="值"
placeholder="属性值"
class="input input-bordered"
>
</div>
@@ -772,10 +764,14 @@
<!-- 组件标签页 -->
<div v-if="activeTab === 'components'">
<h3 class="text-lg font-semibold mb-3">组件编辑</h3>
<textarea
v-model="componentsJson"
class="textarea textarea-bordered w-full h-96 font-mono"
></textarea>
<div class="h-96 border rounded-lg overflow-hidden">
<CodeEditor
v-model:value="componentsJson"
language="json"
theme="vs"
:options="editorOptions"
/>
</div>
<div class="mt-3 flex justify-end">
<button @click="updateComponents" class="btn btn-primary">更新组件</button>
</div>
@@ -793,14 +789,18 @@
>
<input type="checkbox" />
<div class="collapse-title font-semibold">
{{ comp.instanceName ? comp.instanceName.split('.').pop() : '未命名组件' }}
<span class="badge badge-sm badge-neutral ml-2">{{ comp.typePath ? comp.typePath.split('/').pop() : '未知类型' }}</span>
{{ comp.instanceName ? getDisplayName(comp.instanceName).split('.').pop() : '未命名组件' }}
<span class="badge badge-sm badge-neutral ml-2">{{ getTypeName(comp.typePath) || '未知类型' }}</span>
</div>
<div class="collapse-content">
<textarea
v-model="attachedComponentJson[index]"
class="textarea textarea-bordered w-full h-64 font-mono mb-4"
></textarea>
<div class="h-64 border rounded-lg overflow-hidden mb-4">
<CodeEditor
v-model:value="attachedComponentJson[index]"
language="json"
theme="vs"
:options="editorOptions"
/>
</div>
<div class="flex justify-end">
<button @click="updateAttachedComponent(index)" class="btn btn-primary">更新组件</button>
</div>
@@ -832,13 +832,17 @@
</div>
</div>
</template>
<script>
import { ref, computed, watch } from 'vue';
import { Parser } from '@etothepii/satisfactory-file-parser';
import { CodeEditor } from 'monaco-editor-vue3';
export default {
name: 'BlueprintEditor',
components: {
CodeEditor
},
setup() {
// 文件输入引用
const fileInput = ref(null);
@@ -855,6 +859,33 @@ export default {
const activeVisualTab = ref('basic');
const isParsed = ref(false);
// 默认的 sbpcfg 十六进制数据
const defaultSbpcfgHex = '04000000e5ffffff5300610074006900730066006100630074006f00720079002000dd84fe56167f918f6856d89ea48b73006200700063006600670000000e0300000000803f0000803f0000803f0000803f300000002f47616d652f466163746f727947616d652f2d5368617265642f426c75657072696e742f49636f6e4c696272617279000c00000049636f6e4c69627261727900010000000000000000000000000000000000000000';
// 编辑器选项 - 调整行号宽度
const editorOptions = ref({
automaticLayout: true,
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 14,
lineNumbers: 'on',
folding: true,
tabSize: 2,
scrollbar: {
verticalScrollbarSize: 8,
horizontalScrollbarSize: 8
}
});
const smallEditorOptions = ref({
...editorOptions.value,
fontSize: 12,
automaticLayout: true,
minimap: { enabled: false },
tabSize: 2,
});
// 蓝图数据对象
const blueprintData = ref({
name: '',
@@ -897,8 +928,8 @@ export default {
});
// Objects 数据
const rawObjects = ref([]); // 原始读取到的Objects
const newObjects = ref([]); // 编辑后的Objects
const rawObjects = ref([]);
const newObjects = ref([]);
// 使用轻量级对象存储文件信息
const fileInfo = ref({
@@ -927,7 +958,7 @@ export default {
// 计算属性
const canParse = computed(() => {
return fileInfo.value.mainFileName; // 只需要主文件存在
return fileInfo.value.mainFileName;
});
const mainFileName = computed(() => fileInfo.value.mainFileName);
@@ -982,10 +1013,8 @@ export default {
get: () => {
if (!isParsed.value) return '';
// 创建当前JSON的副本
const jsonObj = {
...blueprintData.value,
// 根据showObjects决定是否包含objects
objects: showObjects.value ? newObjects.value : undefined
};
@@ -993,10 +1022,69 @@ export default {
},
set: (value) => {
// 仅存储JSON字符串不自动更新数据模型
// 用户需要手动点击"更新数据"按钮来应用更改
}
});
// 辅助函数检查是否为复杂对象需要JSON编辑器
const isComplexValue = (value) => {
if (value === null || value === undefined) return false;
return typeof value === 'object' && !(value instanceof Array);
};
// 辅助函数:获取属性显示类型
const getPropertyDisplayType = (property) => {
if (!property) return '';
return property.ueType || property.type || '';
};
// 辅助函数:获取属性值用于显示
const getPropertyDisplayValue = (property) => {
if (!property) return '';
if (isComplexValue(property.value)) {
return JSON.stringify(property.value, null, 2);
}
// 处理嵌套的value对象如ByteProperty的情况
if (property.value && typeof property.value === 'object' && 'value' in property.value) {
return property.value.value;
}
return property.value;
};
// 辅助函数:设置属性值
const setPropertyValue = (property, newValue) => {
if (!property) return;
try {
// 如果是复杂对象尝试解析JSON
if (typeof newValue === 'string' && newValue.trim().startsWith('{')) {
property.value = JSON.parse(newValue);
} else if (property.value && typeof property.value === 'object' && 'value' in property.value) {
// 处理嵌套value对象的情况如ByteProperty
property.value.value = newValue;
} else {
property.value = newValue;
}
} catch (e) {
// 如果JSON解析失败保持原值
console.warn('JSON解析失败保持原值:', e);
}
};
// 方法:处理显示名称
const getDisplayName = (instanceName) => {
if (!instanceName) return '';
return instanceName.replace(/^Persistent_Level:PersistentLevel\./, '');
};
// 方法:处理类型名称
const getTypeName = (typePath) => {
if (!typePath) return '';
return typePath.split('/').pop() || typePath;
};
// 方法
const triggerFileInput = () => {
fileInput.value.click();
@@ -1024,7 +1112,6 @@ export default {
return;
}
// 只存储文件名,不存储文件内容
const sbpFile = validFiles.find(f => f.name.endsWith('.sbp'));
const sbpcfgFile = validFiles.find(f => f.name.endsWith('.sbpcfg'));
@@ -1034,7 +1121,6 @@ export default {
configFileName: sbpcfgFile ? sbpcfgFile.name : ''
};
// 只存储文件引用,不存储内容
uploadedFiles.value = validFiles;
};
@@ -1064,7 +1150,6 @@ export default {
if (!canParse.value || isParsing.value) return;
try {
// 重置状态
isParsing.value = true;
statusMessages.value = [];
progress.value = 0;
@@ -1074,11 +1159,9 @@ export default {
rawObjects.value = [];
newObjects.value = [];
// 获取主文件
const sbpFile = uploadedFiles.value.find(f => f.name.endsWith('.sbp'));
const sbpcfgFile = uploadedFiles.value.find(f => f.name.endsWith('.sbpcfg'));
// 读取文件为ArrayBuffer
const readFileAsArrayBuffer = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
@@ -1095,13 +1178,18 @@ export default {
sbpcfgBuffer = await readFileAsArrayBuffer(sbpcfgFile);
statusMessages.value.push('检测到配置文件,将一起解析...');
} else {
statusMessages.value.push('未检测到配置文件,将仅解析主文件...');
statusMessages.value.push('未检测到配置文件,将使用默认配置...');
const hexString = defaultSbpcfgHex;
const buffer = new ArrayBuffer(hexString.length / 2);
const view = new Uint8Array(buffer);
for (let i = 0; i < hexString.length; i += 2) {
view[i / 2] = parseInt(hexString.substring(i, i + 2), 16);
}
sbpcfgBuffer = buffer;
}
// 更新状态
statusMessages.value.push('开始解析蓝图文件...');
// 解析蓝图
const blueprint = await Parser.ParseBlueprintFiles(
fileInfo.value.blueprintName,
sbpBuffer,
@@ -1114,13 +1202,9 @@ export default {
}
);
// 存储原始objects数据
rawObjects.value = blueprint.objects || [];
// 初始化newObjects为rawObjects的副本
newObjects.value = JSON.parse(JSON.stringify(rawObjects.value));
// 提取蓝图数据
blueprintData.value = {
name: blueprint.name || '',
compressionInfo: {
@@ -1183,20 +1267,32 @@ export default {
isExporting.value = true;
exportStatus.value = '开始导出蓝图文件...';
// 创建当前JSON对象
const currentJson = {
...blueprintData.value,
// 根据showObjects状态决定使用哪个objects数据
objects: showObjects.value ? newObjects.value : rawObjects.value
};
// 创建当前时间戳
const timestamp = Date.now().toString();
// 准备导出的数据 - 深拷贝避免修改原始数据
const exportData = JSON.parse(JSON.stringify({
...blueprintData.value,
objects: newObjects.value
}));
// 确保 lastEditedBy 数组存在
if (!exportData.config.lastEditedBy) {
exportData.config.lastEditedBy = [];
}
// 在数组尾部添加新的编辑者信息(第二位)
exportData.config.lastEditedBy.push({
accountId: timestamp,
displayName: "LinXingLuo",
platformName: "Satisfactory-edit"
});
// 准备导出变量
let mainFileHeader = null;
const mainFileBodyChunks = [];
// 导出蓝图
const summary = await Parser.WriteBlueprintFiles(
currentJson,
exportData,
header => {
mainFileHeader = header;
},
@@ -1205,13 +1301,11 @@ export default {
}
);
// 组合主文件
const mainFileData = new Uint8Array([
...mainFileHeader,
...mainFileBodyChunks.flatMap(chunk => [...chunk])
]);
// 创建下载链接
const downloadFile = (data, filename, type) => {
const blob = new Blob([data], { type });
const url = URL.createObjectURL(blob);
@@ -1224,7 +1318,6 @@ export default {
URL.revokeObjectURL(url);
};
// 下载文件
downloadFile(mainFileData, `${blueprintName.value}.sbp`, 'application/octet-stream');
downloadFile(summary.configFileBinary, `${blueprintName.value}.sbpcfg`, 'application/octet-stream');
@@ -1247,9 +1340,7 @@ export default {
}
};
// 刷新JSON显示
const refreshJson = () => {
// 创建一个新的JSON对象根据showObjects状态决定是否包含objects
const jsonObj = {
...blueprintData.value,
objects: showObjects.value ? newObjects.value : undefined
@@ -1258,19 +1349,16 @@ export default {
displayedJson.value = JSON.stringify(jsonObj, null, 2);
};
// 从JSON更新数据
const updateDataFromJson = () => {
try {
const editedObj = JSON.parse(displayedJson.value);
// 更新blueprintData除了objects
for (const key in editedObj) {
if (key !== 'objects') {
blueprintData.value[key] = editedObj[key];
}
}
// 如果JSON中包含objects字段则更新newObjects
if (editedObj.objects) {
newObjects.value = editedObj.objects;
}
@@ -1293,7 +1381,6 @@ export default {
const updateEntityFromJson = () => {
try {
const updatedEntity = JSON.parse(entityJson.value);
// 找到实体在原始数组中的位置
const originalIndex = newObjects.value.findIndex(
e => e.instanceName === selectedEntity.value.instanceName
);
@@ -1344,17 +1431,6 @@ export default {
const addProperty = () => {
if (!newProperty.value.name || !selectedEntity.value) return;
// 处理ObjectProperty类型
let value = newProperty.value.value;
if (newProperty.value.type === 'ObjectProperty') {
try {
value = JSON.parse(newProperty.value.value);
} catch (e) {
value = { levelName: "", pathName: "" };
}
}
// 确保properties对象存在
if (!selectedEntity.value.properties) {
selectedEntity.value.properties = {};
}
@@ -1363,15 +1439,13 @@ export default {
type: newProperty.value.type,
ueType: newProperty.value.type,
name: newProperty.value.name,
value: value
value: newProperty.value.value
};
// 初始化新属性的JSON字符串
if (newProperty.value.type === 'ObjectProperty' || newProperty.value.type === 'StructProperty') {
objectProperties.value[newProperty.value.name] = JSON.stringify(value, null, 2);
if (isComplexValue(newProperty.value.value)) {
objectProperties.value[newProperty.value.name] = JSON.stringify(newProperty.value.value, null, 2);
}
// 重置表单
newProperty.value = {
name: '',
type: 'FloatProperty',
@@ -1385,7 +1459,7 @@ export default {
objectProperties.value = {};
for (const key in selectedEntity.value.properties) {
const prop = selectedEntity.value.properties[key];
if (prop.ueType === 'ObjectProperty' || prop.ueType === 'StructProperty') {
if (isComplexValue(prop.value)) {
objectProperties.value[key] = JSON.stringify(prop.value, null, 2);
}
}
@@ -1422,7 +1496,6 @@ export default {
// 监听newObjects变化
watch(newObjects, () => {
// 当newObjects变化时刷新附属组件
if (selectedEntity.value) {
initAttachedComponentJson();
}
@@ -1447,6 +1520,8 @@ export default {
mainFileName,
configFileName,
isParsed,
editorOptions,
smallEditorOptions,
// 第一个文件编辑器相关
filteredEntities,
@@ -1461,6 +1536,12 @@ export default {
componentsJson,
attachedComponents,
// 辅助函数
isComplexValue,
getPropertyDisplayType,
getPropertyDisplayValue,
setPropertyValue,
// 方法
triggerFileInput,
handleFileChange,
@@ -1475,6 +1556,8 @@ export default {
removeRecipe,
addItemCost,
removeItemCost,
getDisplayName,
getTypeName,
// 第一个文件编辑器方法
selectEntity,
@@ -1486,86 +1569,4 @@ export default {
};
}
}
</script>
<style scoped>
.container {
min-height: 100vh;
}
.card-title {
margin-bottom: 1rem;
}
.progress {
transition: width 0.3s ease;
}
.json-editor {
min-height: 400px;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
}
.alert {
margin-top: 1rem;
}
.badge {
margin-left: 0.5rem;
}
.large-file-warning {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(255, 0, 0, 0); }
100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); }
}
.dimension-input {
width: 80px;
}
.color-preview {
width: 30px;
height: 30px;
border-radius: 4px;
display: inline-block;
vertical-align: middle;
margin-right: 10px;
}
.recipe-item {
border-radius: 4px;
padding: 8px;
margin-bottom: 8px;
}
.item-cost {
border-radius: 4px;
padding: 8px;
margin-bottom: 8px;
}
.editor-tabs .tab {
padding: 12px 20px;
}
.tab[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
.object-list-item {
transition: all 0.2s;
}
.object-list-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
</style>
</script>

View File

@@ -1,43 +0,0 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@@ -1,2 +1,2 @@
@import "tailwindcss";
@plugin "daisyui";
@plugin "daisyui";