init
6
.env.development
Normal file
@ -0,0 +1,6 @@
|
||||
ENV = 'production'
|
||||
VUE_APP_MODE = 'production'
|
||||
VITE_BASE_URL = 'https://wly-demo.wlyun.co'
|
||||
VITE_ENVURL='https://wlyun-public.oss-cn-hangzhou.aliyuncs.com/shuangxi/hdr/'
|
||||
VITE_MODELURL='https://wlyun-public.oss-cn-hangzhou.aliyuncs.com/shuangxi/model/'
|
||||
VITE_PUBLIC_URL=/
|
||||
6
.env.production
Normal file
@ -0,0 +1,6 @@
|
||||
ENV = 'production'
|
||||
VUE_APP_MODE = 'production'
|
||||
VITE_BASE_URL = 'https://wly-demo.wlyun.co'
|
||||
VITE_ENVURL='https://wlyun-public.oss-cn-hangzhou.aliyuncs.com/shuangxi/hdr/'
|
||||
VITE_MODELURL='https://wlyun-public.oss-cn-hangzhou.aliyuncs.com/shuangxi/model/'
|
||||
VITE_PUBLIC_URL=./
|
||||
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
14
.gitlab-ci.yml
Normal file
@ -0,0 +1,14 @@
|
||||
# This file is a template, and might need editing before it works on your project.
|
||||
# Full project: https://gitlab.com/pages/plain-html
|
||||
|
||||
image: busybox
|
||||
|
||||
pages:
|
||||
stage: deploy
|
||||
script:
|
||||
- echo "The site will be deployed to $CI_PAGES_URL"
|
||||
artifacts:
|
||||
paths:
|
||||
- public
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
106
README.md
Normal file
@ -0,0 +1,106 @@
|
||||
# Vue 3 + TypeScript + Vite + BabylonJS 项目模板
|
||||
|
||||
## 项目简介
|
||||
|
||||
这是一个基于 Vue 3、TypeScript 和 Vite 构建的 3D 可视化项目。本项目使用 BabylonJS 作为 3D 引擎,结合 Vue 3 的 `<script setup>` 语法糖,提供了现代化的 Web3D 开发环境。
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 核心框架
|
||||
- Vue 3 - 渐进式 JavaScript 框架
|
||||
- TypeScript - JavaScript 的超集,添加了类型系统
|
||||
- Vite - 下一代前端构建工具
|
||||
|
||||
### 3D 引擎与工具
|
||||
- BabylonJS 7.32 - 强大的 Web3D 引擎
|
||||
- BabylonJS Loaders - 用于加载 3D 模型和资源
|
||||
- Stats.js - 性能监控工具
|
||||
|
||||
### UI 框架
|
||||
- Element Plus - 基于 Vue 3 的组件库
|
||||
- Vant - 移动端组件库
|
||||
|
||||
### 开发工具
|
||||
- GLSL 着色器支持 (vite-plugin-glsl)
|
||||
- Monaco Editor - 代码编辑器集成
|
||||
- Sass - CSS 预处理器
|
||||
|
||||
## 项目功能
|
||||
|
||||
- 3D 场景渲染与交互
|
||||
- 自定义着色器支持(shader 目录)
|
||||
- 性能监控和优化
|
||||
- 响应式界面设计
|
||||
- 代码编辑器集成
|
||||
|
||||
## 开发环境设置
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Node.js 版本 >= 14.0.0
|
||||
- npm 版本 >= 6.0.0
|
||||
|
||||
### 推荐的 IDE 配置
|
||||
|
||||
- [VSCode](https://code.visualstudio.com/) - 轻量级但功能强大的编辑器
|
||||
- [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) - Vue 3 的官方 VSCode 插件
|
||||
|
||||
## 项目启动步骤
|
||||
|
||||
1. 安装依赖
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. 启动开发服务器
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
3. 构建生产版本
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/ # API 接口定义
|
||||
├── assets/ # 静态资源文件
|
||||
├── components/ # Vue 组件
|
||||
├── global/ # 全局配置和工具
|
||||
├── shader/ # GLSL 着色器文件
|
||||
├── types/ # TypeScript 类型定义
|
||||
├── view/ # 页面视图组件
|
||||
└── App.vue # 根组件
|
||||
```
|
||||
|
||||
## 开发规范
|
||||
|
||||
- 组件文件使用 PascalCase 命名
|
||||
- TypeScript 文件使用 camelCase 命名
|
||||
- 样式文件使用 kebab-case 命名
|
||||
- 着色器文件使用 .glsl 或 .shader 扩展名
|
||||
|
||||
## 性能优化
|
||||
|
||||
- 使用 vite-plugin-compression 进行资源压缩
|
||||
- 通过 Stats.js 监控性能指标
|
||||
- 支持代码分割和懒加载
|
||||
- 针对移动端的优化配置
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 开发时请确保启用了 Volar 并禁用了 Vetur
|
||||
- 建议使用 Vue 3 的组合式 API 进行开发
|
||||
- 3D 模型推荐使用 glTF/glb 格式
|
||||
- 确保在提交代码前运行类型检查和代码格式化
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [Vue 3 文档](https://v3.cn.vuejs.org/)
|
||||
- [BabylonJS 文档](https://doc.babylonjs.com/)
|
||||
- [TypeScript 文档](https://www.typescriptlang.org/)
|
||||
- [Element Plus 文档](https://element-plus.org/zh-CN/)
|
||||
- [Vite 文档](https://cn.vitejs.dev/)
|
||||
16
index.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>测试</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
9570
package-lock.json
generated
Normal file
44
package.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babylonjs/core": "^7.54.3",
|
||||
"@babylonjs/loaders": "^7.54.3",
|
||||
"@openai/codex": "^0.1.2505291658",
|
||||
"axios": "^1.2.1",
|
||||
"earcut": "^3.0.1",
|
||||
"editor": "^1.0.0",
|
||||
"element-plus": "^2.9.9",
|
||||
"mitt": "^3.0.1",
|
||||
"pinia": "^3.0.2",
|
||||
"popmotion": "^11.0.5",
|
||||
"stats.js": "^0.17.0",
|
||||
"uuid": "^10.0.0",
|
||||
"vant": "^4.0.2",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue": "^3.2.25"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.19.67",
|
||||
"@types/stats.js": "^0.17.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-legacy": "^2.0.0",
|
||||
"@vitejs/plugin-vue": "^3.0.1",
|
||||
"rollup-plugin-visualizer": "^5.14.0",
|
||||
"sass": "^1.54.9",
|
||||
"sass-loader": "^13.0.2",
|
||||
"sass-resources-loader": "^2.2.5",
|
||||
"terser": "^5.14.2",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^3.2.11",
|
||||
"vue-tsc": "^0.34.17",
|
||||
"webpack-bundle-analyzer": "^4.10.2"
|
||||
}
|
||||
}
|
||||
21
public/basis/basis_transcoder.js
Normal file
BIN
public/basis/basis_transcoder.wasm
Normal file
7
public/config/Global.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"publicURL": "",
|
||||
"freeHiglight": true,
|
||||
"panningSensibility": 2000,
|
||||
"clickReturnOrigin": false
|
||||
|
||||
}
|
||||
33
public/config/ProductType.json
Normal file
@ -0,0 +1,33 @@
|
||||
[
|
||||
{
|
||||
"id": "2e0f716c-1d3b-47f8-9374-780d97a6b7f7",
|
||||
"code": "dt-01",
|
||||
"name": "直梯",
|
||||
"modelUrl": "src/model/car3.glb",
|
||||
"thumbnailUrl": "src/直梯10.png",
|
||||
"paths": [
|
||||
"config/x1/Component.json"
|
||||
|
||||
],
|
||||
"vr": true,
|
||||
"contrast":true,
|
||||
"cameraPosition":[7.52,1.88,-5.24],
|
||||
"cameraTarget":[0,0.5,0.2],
|
||||
"angularSensibility":1000
|
||||
},
|
||||
{
|
||||
"id": "2e0f716c-155b-47f8-9374-5a0d97a6b7f7",
|
||||
"code": "dt-02",
|
||||
"name": "扶梯",
|
||||
"modelUrl": "src/model/日立扶梯.glb",
|
||||
"thumbnailUrl": "src/扶梯10.png",
|
||||
"paths": [
|
||||
"config/扶梯/Component.json"
|
||||
],
|
||||
"vr": false,
|
||||
"contrast":true,
|
||||
"cameraPosition":[0,0,70],
|
||||
"cameraTarget":[0,0.2,0],
|
||||
"angularSensibility":1500
|
||||
}
|
||||
]
|
||||
149
public/config/x1/Component.json
Normal file
@ -0,0 +1,149 @@
|
||||
[
|
||||
{
|
||||
"name": "座椅白_N_T",
|
||||
"albedoTexture": "",
|
||||
"alphaTexture": "",
|
||||
"bumpTexture": "src/texture/座椅白_N_T.jpg",
|
||||
"aoTexture": ""
|
||||
},
|
||||
{
|
||||
"name": "后尾灯红色内部_N_T",
|
||||
"albedoTexture": "",
|
||||
"alphaTexture": "",
|
||||
"bumpTexture": "src/texture/车门黄灯_N_T.ktx2",
|
||||
"aoTexture": ""
|
||||
},
|
||||
{
|
||||
"name": "后尾灯黄色内部_N_T",
|
||||
"albedoTexture": "",
|
||||
"alphaTexture": "",
|
||||
"bumpTexture": "src/texture/车门黄灯_N_T.ktx2",
|
||||
"aoTexture": ""
|
||||
},
|
||||
{
|
||||
"name": "挡泥板标_T",
|
||||
"albedoTexture": "src/texture/挡泥板标_T.png",
|
||||
"alphaTexture": "src/texture/挡泥板标_T.png",
|
||||
"bumpTexture": "",
|
||||
"aoTexture": ""
|
||||
},
|
||||
{
|
||||
"name": "方向盘磨砂_N_T",
|
||||
"albedoTexture": "",
|
||||
"alphaTexture": "",
|
||||
"bumpTexture": "src/texture/座椅白_N_T.ktx2",
|
||||
"aoTexture": ""
|
||||
},
|
||||
{
|
||||
"name": "红条_T",
|
||||
"albedoTexture": "src/texture/红条2_T.ktx2",
|
||||
"alphaTexture": "",
|
||||
"bumpTexture": "",
|
||||
"aoTexture": ""
|
||||
},
|
||||
{
|
||||
"name": "蓝色-前脸_AO_T",
|
||||
"albedoTexture": "",
|
||||
"alphaTexture": "",
|
||||
"bumpTexture": "",
|
||||
"aoTexture": "src/texture/蓝色前脸_Mixed_AO2.ktx2"
|
||||
},
|
||||
{
|
||||
"name": "藍色--车顶侧_AO_T",
|
||||
"albedoTexture": "",
|
||||
"alphaTexture": "",
|
||||
"bumpTexture": "",
|
||||
"aoTexture": "src/texture/车侧顶AO.ktx2"
|
||||
},
|
||||
{
|
||||
"name": "车厢_AO_T",
|
||||
"albedoTexture": "",
|
||||
"alphaTexture": "",
|
||||
"bumpTexture": "",
|
||||
"aoTexture": "src/texture/车厢_Mixed_AO2.ktx2"
|
||||
},
|
||||
{
|
||||
"name": "车厢2_AO_T",
|
||||
"albedoTexture": "",
|
||||
"alphaTexture": "",
|
||||
"bumpTexture": "",
|
||||
"aoTexture": "src/texture/车厢.001_Mixed_AO2.ktx2"
|
||||
},
|
||||
{
|
||||
"name": "蓝色-门_AO_T",
|
||||
"albedoTexture": "",
|
||||
"alphaTexture": "",
|
||||
"bumpTexture": "",
|
||||
"aoTexture": "src/texture/蓝色-门_Mixed_AO2.ktx2"
|
||||
},
|
||||
{
|
||||
"name": "右门标_T",
|
||||
"albedoTexture": "src/texture/车门标.ktx2",
|
||||
"alphaTexture": "src/texture/车门标.ktx2",
|
||||
"bumpTexture": "",
|
||||
"aoTexture": ""
|
||||
},
|
||||
{
|
||||
"name": "右门标2_T",
|
||||
"albedoTexture": "src/texture/右门标_T.ktx2",
|
||||
"alphaTexture": "src/texture/右门标_T.ktx2",
|
||||
"bumpTexture": "",
|
||||
"aoTexture": ""
|
||||
},
|
||||
{
|
||||
"name": "车前脸标_T",
|
||||
"albedoTexture": "",
|
||||
"alphaTexture": "src/texture/车前脸标_T.ktx2",
|
||||
"bumpTexture": "",
|
||||
"aoTexture": ""
|
||||
},
|
||||
{
|
||||
"name": "红条_T.001",
|
||||
"albedoTexture": "src/texture/红条2_T.ktx2",
|
||||
"alphaTexture": "",
|
||||
"bumpTexture": "",
|
||||
"aoTexture": ""
|
||||
},
|
||||
{
|
||||
"name": "车后标2_T",
|
||||
"albedoTexture": "",
|
||||
"alphaTexture": "src/texture/车后标2_T.ktx2",
|
||||
"bumpTexture": "",
|
||||
"aoTexture": ""
|
||||
},
|
||||
{
|
||||
"name": "车后标_T",
|
||||
"albedoTexture": "",
|
||||
"alphaTexture": "src/texture/车后标_T.ktx2",
|
||||
"bumpTexture": "",
|
||||
"aoTexture": ""
|
||||
},
|
||||
{
|
||||
"name": "藍色.005",
|
||||
"albedoTexture": "src/texture/车牌_T.png",
|
||||
"alphaTexture": "",
|
||||
"bumpTexture": "",
|
||||
"aoTexture": ""
|
||||
},
|
||||
{
|
||||
"name": "内饰磨砂_N_T",
|
||||
"albedoTexture": "",
|
||||
"alphaTexture": "",
|
||||
"bumpTexture": "src/texture/座椅白_N_T.jpg",
|
||||
"aoTexture": ""
|
||||
},
|
||||
{
|
||||
"name": "仪表盘_D_T",
|
||||
"albedoTexture": "src/texture/仪表盘_D_T.ktx2",
|
||||
"alphaTexture": "",
|
||||
"bumpTexture": "",
|
||||
"aoTexture": ""
|
||||
},
|
||||
{
|
||||
"name": "内饰显示屏_D_T",
|
||||
"albedoTexture": "src/texture/内饰显示屏_D_T.ktx2",
|
||||
"alphaTexture": "",
|
||||
"bumpTexture": "",
|
||||
"aoTexture": ""
|
||||
}
|
||||
]
|
||||
193
public/config/x2/Component.json
Normal file
@ -0,0 +1,193 @@
|
||||
[
|
||||
{
|
||||
"id": "2beebcaf-8851-4690-b156-252124ff788",
|
||||
"name": "10成新1",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "2beeb3af-8851-4690-b166-2526a1sff788",
|
||||
"albedoTexture": "src/texture1/外观/扶手10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/外观/扶手10_RoughnessM.ktx2",
|
||||
"transparent": -1
|
||||
},
|
||||
{
|
||||
"id": "2beebcaf-8851-4690-b156-552124f6788",
|
||||
"name": "10成新1",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "2beeb3af-8851-4690-b166-56564543f788",
|
||||
"albedoTexture": "src/texture1/外观/栏板10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/外观/栏板10_RoughnessM.ktx2",
|
||||
"transparent": -1
|
||||
},
|
||||
{
|
||||
"id": "34eebcaf-8851-4690-b156-34343455667",
|
||||
"name": "10成新18",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "1235b3af-8871-2690-b166-453456111f788",
|
||||
"albedoTexture": "src/texture1/外观/楼层板10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/外观/楼层板10_RoughnessM.ktx2",
|
||||
"transparent": -1
|
||||
},
|
||||
{
|
||||
"id": "11eebcaf-8821-4690-b136-252641sff788",
|
||||
"name": "10成新2",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "1beeb3af-8821-4690-b136-252641sff788",
|
||||
"albedoTexture": "src/texture1/外观/玻璃10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/外观/玻璃10_RoughnessM.ktx2",
|
||||
"transparent": 0.5
|
||||
},
|
||||
{
|
||||
"id": "11eebcaf-8821-4690-b136-252641sf1111",
|
||||
"name": "10成新2",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "1235b3af-8871-2690-b166-2323231111f788",
|
||||
"albedoTexture": "src/texture1/外观/群板盖板10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/外观/群板盖板10_RoughnessM.ktx2",
|
||||
"transparent": -1
|
||||
},
|
||||
{
|
||||
"id": "12eebcaf-8871-4690-b166-2586a1sff788",
|
||||
"name": "10成新3",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "25eeb3af-8871-4690-b166-2586a1sff788",
|
||||
"albedoTexture": "src/texture1/外观/梯级/梯级10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/外观/梯级/梯级10_RoughnessM.ktx2",
|
||||
"transparent": -1
|
||||
},
|
||||
{
|
||||
"id": "13113caf-8871-2690-b166-2486a1sff788",
|
||||
"name": "10成新4",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "2323233-8821-4690-b136-252641sff788",
|
||||
"albedoTexture": "src/texture1/扶梯主机/扶梯主机10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/扶梯主机/扶梯主机10_RoughnessMetallic.ktx2",
|
||||
"transparent": -1
|
||||
},
|
||||
{
|
||||
"id": "1422bcaf-8851-4690-b166-2526a1sff788",
|
||||
"name": "10成新5",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "1b33b3af-8821-4690-b136-252641s11111",
|
||||
"albedoTexture": "src/texture1/单双链/单链10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/单双链/单链10_RoughnessMF.ktx2",
|
||||
"transparent": -1
|
||||
},
|
||||
{
|
||||
"id": "1533bcaf-8821-4690-b136-252641sff788",
|
||||
"name": "10成新6",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "2544b3af-8871-4690-b166-2586a1sff788",
|
||||
"albedoTexture": "src/texture1/附加制动器/附加制动器10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/附加制动器/附加制动器10_RoughnessMF.ktx2",
|
||||
"transparent": -1
|
||||
},
|
||||
{
|
||||
"id": "2333bcaf-8821-4690-b136-252641sff788",
|
||||
"name": "10成新6",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "2555b3af-8871-2690-b166-2486a1sff788",
|
||||
"albedoTexture": "src/texture1/桁架/桁架10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/桁架/桁架10_RoughnessMetallic.ktx2",
|
||||
"transparent": -1
|
||||
},
|
||||
{
|
||||
"id": "1755bcaf-8871-2690-b166-2486a1sff788",
|
||||
"name": "10成新7",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "1235b3af-8871-2690-b166-2486a1111f788",
|
||||
"albedoTexture": "src/texture1/单双链/双链10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/单双链/双链10_RoughnessMF.ktx2",
|
||||
"transparent": -1
|
||||
},
|
||||
{
|
||||
"id": "1755bcaf-8871-2690-b166-24823132a1sff788",
|
||||
"name": "10成新8",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "2b66b3af-8851-4690-b166-2526a1sff788",
|
||||
"albedoTexture": "src/texture1/轴承/轴承10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/轴承/轴承10_RoughnessMF.ktx2",
|
||||
"transparent": -1
|
||||
},
|
||||
{
|
||||
"id": "34eebcaf-8851-4690-b156-2526a1412128",
|
||||
"name": "10成新18",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "1b77b3af-8821-4690-b136-252641sff788",
|
||||
"albedoTexture": "src/texture1/其他/制动器/制动器10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/其他/制动器/制动器10_RoughnessMF.ktx2",
|
||||
"transparent": -1
|
||||
},
|
||||
{
|
||||
"id": "1866bcaf-8851-4690-b166-2526a1sff788",
|
||||
"name": "10成新9",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "2588b3af-8871-4690-b166-2586a1sff788",
|
||||
"albedoTexture": "其他/其他支架/其他支架10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "其他/其他支架/其他支架10_RoughnessMF.ktx2",
|
||||
"transparent": -1
|
||||
},
|
||||
{
|
||||
"id": "1977bcaf-8851-4690-b166-2526a1sff788",
|
||||
"name": "10成新10",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "2599b3af-8871-2690-b166-22345566ff788",
|
||||
"albedoTexture": "src/texture1/其他/其他下/下10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/其他/其他下/下10_RoughnessMF.ktx2",
|
||||
"transparent": -1
|
||||
},
|
||||
{
|
||||
"id": "2199bcaf-8851-4690-b166-2526a1sff788",
|
||||
"name": "10成新12",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "1b12b3af-8821-4690-b136-252641sff788",
|
||||
"albedoTexture": "src/texture1/其他/其他上/上10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/其他/其他上/上10_RoughnessMF.ktx2",
|
||||
"transparent": -1
|
||||
},
|
||||
{
|
||||
"id": "676766663-8871-4690-b166-2586a1sff788",
|
||||
"name": "5成新3",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "56666555-8821-4690-b136-252641sff788",
|
||||
"albedoTexture": "src/texture1/控制系统/控制柜/控制柜10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/控制系统/控制柜/控制柜10_RoughnessMF.ktx2",
|
||||
"transparent": -1
|
||||
},
|
||||
{
|
||||
"id": "67844434445-8871-4690-b166-2586a1sff788",
|
||||
"name": "5成新3",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "1234567438821-4690-b136-252641sff788",
|
||||
"albedoTexture": "src/texture1/控制系统/油泵/油泵10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/控制系统/油泵/油泵10_RoughnessMF.ktx2",
|
||||
"transparent": -1
|
||||
}
|
||||
,
|
||||
{
|
||||
"id": "tgdrfg45435-8871-4690-b166-2586a1sff788",
|
||||
"name": "5成新3",
|
||||
"degreeId": "8e56631f-ab2f-4574-a969-7f75f8b69903",
|
||||
"materialId": "6785543333-4690-b136-252641sff788",
|
||||
"albedoTexture": "src/texture1/控制系统/油泵/透明10_BaseColor.ktx2",
|
||||
"bumpTexture": "",
|
||||
"roughnessTexture": "src/texture1/控制系统/油泵/透明10_RoughnessMF.ktx2",
|
||||
"transparent": 0.6
|
||||
}
|
||||
]
|
||||
35
public/draco/draco_decoder_gltf.js
Normal file
BIN
public/draco/draco_decoder_gltf.wasm
Normal file
116
public/draco/draco_wasm_wrapper_gltf.js
Normal file
@ -0,0 +1,116 @@
|
||||
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(h){var n=0;return function(){return n<h.length?{done:!1,value:h[n++]}:{done:!0}}};$jscomp.arrayIterator=function(h){return{next:$jscomp.arrayIteratorImpl(h)}};$jscomp.makeIterator=function(h){var n="undefined"!=typeof Symbol&&Symbol.iterator&&h[Symbol.iterator];return n?n.call(h):$jscomp.arrayIterator(h)};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;
|
||||
$jscomp.ISOLATE_POLYFILLS=!1;$jscomp.FORCE_POLYFILL_PROMISE=!1;$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION=!1;$jscomp.getGlobal=function(h){h=["object"==typeof globalThis&&globalThis,h,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var n=0;n<h.length;++n){var k=h[n];if(k&&k.Math==Math)return k}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
|
||||
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(h,n,k){if(h==Array.prototype||h==Object.prototype)return h;h[n]=k.value;return h};$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";
|
||||
var $jscomp$lookupPolyfilledValue=function(h,n){var k=$jscomp.propertyToPolyfillSymbol[n];if(null==k)return h[n];k=h[k];return void 0!==k?k:h[n]};$jscomp.polyfill=function(h,n,k,p){n&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(h,n,k,p):$jscomp.polyfillUnisolated(h,n,k,p))};
|
||||
$jscomp.polyfillUnisolated=function(h,n,k,p){k=$jscomp.global;h=h.split(".");for(p=0;p<h.length-1;p++){var l=h[p];if(!(l in k))return;k=k[l]}h=h[h.length-1];p=k[h];n=n(p);n!=p&&null!=n&&$jscomp.defineProperty(k,h,{configurable:!0,writable:!0,value:n})};
|
||||
$jscomp.polyfillIsolated=function(h,n,k,p){var l=h.split(".");h=1===l.length;p=l[0];p=!h&&p in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var y=0;y<l.length-1;y++){var f=l[y];if(!(f in p))return;p=p[f]}l=l[l.length-1];k=$jscomp.IS_SYMBOL_NATIVE&&"es6"===k?p[l]:null;n=n(k);null!=n&&(h?$jscomp.defineProperty($jscomp.polyfills,l,{configurable:!0,writable:!0,value:n}):n!==k&&(void 0===$jscomp.propertyToPolyfillSymbol[l]&&(k=1E9*Math.random()>>>0,$jscomp.propertyToPolyfillSymbol[l]=$jscomp.IS_SYMBOL_NATIVE?
|
||||
$jscomp.global.Symbol(l):$jscomp.POLYFILL_PREFIX+k+"$"+l),$jscomp.defineProperty(p,$jscomp.propertyToPolyfillSymbol[l],{configurable:!0,writable:!0,value:n})))};
|
||||
$jscomp.polyfill("Promise",function(h){function n(){this.batch_=null}function k(f){return f instanceof l?f:new l(function(q,u){q(f)})}if(h&&(!($jscomp.FORCE_POLYFILL_PROMISE||$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION&&"undefined"===typeof $jscomp.global.PromiseRejectionEvent)||!$jscomp.global.Promise||-1===$jscomp.global.Promise.toString().indexOf("[native code]")))return h;n.prototype.asyncExecute=function(f){if(null==this.batch_){this.batch_=[];var q=this;this.asyncExecuteFunction(function(){q.executeBatch_()})}this.batch_.push(f)};
|
||||
var p=$jscomp.global.setTimeout;n.prototype.asyncExecuteFunction=function(f){p(f,0)};n.prototype.executeBatch_=function(){for(;this.batch_&&this.batch_.length;){var f=this.batch_;this.batch_=[];for(var q=0;q<f.length;++q){var u=f[q];f[q]=null;try{u()}catch(A){this.asyncThrow_(A)}}}this.batch_=null};n.prototype.asyncThrow_=function(f){this.asyncExecuteFunction(function(){throw f;})};var l=function(f){this.state_=0;this.result_=void 0;this.onSettledCallbacks_=[];this.isRejectionHandled_=!1;var q=this.createResolveAndReject_();
|
||||
try{f(q.resolve,q.reject)}catch(u){q.reject(u)}};l.prototype.createResolveAndReject_=function(){function f(A){return function(F){u||(u=!0,A.call(q,F))}}var q=this,u=!1;return{resolve:f(this.resolveTo_),reject:f(this.reject_)}};l.prototype.resolveTo_=function(f){if(f===this)this.reject_(new TypeError("A Promise cannot resolve to itself"));else if(f instanceof l)this.settleSameAsPromise_(f);else{a:switch(typeof f){case "object":var q=null!=f;break a;case "function":q=!0;break a;default:q=!1}q?this.resolveToNonPromiseObj_(f):
|
||||
this.fulfill_(f)}};l.prototype.resolveToNonPromiseObj_=function(f){var q=void 0;try{q=f.then}catch(u){this.reject_(u);return}"function"==typeof q?this.settleSameAsThenable_(q,f):this.fulfill_(f)};l.prototype.reject_=function(f){this.settle_(2,f)};l.prototype.fulfill_=function(f){this.settle_(1,f)};l.prototype.settle_=function(f,q){if(0!=this.state_)throw Error("Cannot settle("+f+", "+q+"): Promise already settled in state"+this.state_);this.state_=f;this.result_=q;2===this.state_&&this.scheduleUnhandledRejectionCheck_();
|
||||
this.executeOnSettledCallbacks_()};l.prototype.scheduleUnhandledRejectionCheck_=function(){var f=this;p(function(){if(f.notifyUnhandledRejection_()){var q=$jscomp.global.console;"undefined"!==typeof q&&q.error(f.result_)}},1)};l.prototype.notifyUnhandledRejection_=function(){if(this.isRejectionHandled_)return!1;var f=$jscomp.global.CustomEvent,q=$jscomp.global.Event,u=$jscomp.global.dispatchEvent;if("undefined"===typeof u)return!0;"function"===typeof f?f=new f("unhandledrejection",{cancelable:!0}):
|
||||
"function"===typeof q?f=new q("unhandledrejection",{cancelable:!0}):(f=$jscomp.global.document.createEvent("CustomEvent"),f.initCustomEvent("unhandledrejection",!1,!0,f));f.promise=this;f.reason=this.result_;return u(f)};l.prototype.executeOnSettledCallbacks_=function(){if(null!=this.onSettledCallbacks_){for(var f=0;f<this.onSettledCallbacks_.length;++f)y.asyncExecute(this.onSettledCallbacks_[f]);this.onSettledCallbacks_=null}};var y=new n;l.prototype.settleSameAsPromise_=function(f){var q=this.createResolveAndReject_();
|
||||
f.callWhenSettled_(q.resolve,q.reject)};l.prototype.settleSameAsThenable_=function(f,q){var u=this.createResolveAndReject_();try{f.call(q,u.resolve,u.reject)}catch(A){u.reject(A)}};l.prototype.then=function(f,q){function u(w,B){return"function"==typeof w?function(R){try{A(w(R))}catch(Z){F(Z)}}:B}var A,F,v=new l(function(w,B){A=w;F=B});this.callWhenSettled_(u(f,A),u(q,F));return v};l.prototype.catch=function(f){return this.then(void 0,f)};l.prototype.callWhenSettled_=function(f,q){function u(){switch(A.state_){case 1:f(A.result_);
|
||||
break;case 2:q(A.result_);break;default:throw Error("Unexpected state: "+A.state_);}}var A=this;null==this.onSettledCallbacks_?y.asyncExecute(u):this.onSettledCallbacks_.push(u);this.isRejectionHandled_=!0};l.resolve=k;l.reject=function(f){return new l(function(q,u){u(f)})};l.race=function(f){return new l(function(q,u){for(var A=$jscomp.makeIterator(f),F=A.next();!F.done;F=A.next())k(F.value).callWhenSettled_(q,u)})};l.all=function(f){var q=$jscomp.makeIterator(f),u=q.next();return u.done?k([]):new l(function(A,
|
||||
F){function v(R){return function(Z){w[R]=Z;B--;0==B&&A(w)}}var w=[],B=0;do w.push(void 0),B++,k(u.value).callWhenSettled_(v(w.length-1),F),u=q.next();while(!u.done)})};return l},"es6","es3");$jscomp.owns=function(h,n){return Object.prototype.hasOwnProperty.call(h,n)};$jscomp.assign=$jscomp.TRUST_ES6_POLYFILLS&&"function"==typeof Object.assign?Object.assign:function(h,n){for(var k=1;k<arguments.length;k++){var p=arguments[k];if(p)for(var l in p)$jscomp.owns(p,l)&&(h[l]=p[l])}return h};
|
||||
$jscomp.polyfill("Object.assign",function(h){return h||$jscomp.assign},"es6","es3");$jscomp.checkStringArgs=function(h,n,k){if(null==h)throw new TypeError("The 'this' value for String.prototype."+k+" must not be null or undefined");if(n instanceof RegExp)throw new TypeError("First argument to String.prototype."+k+" must not be a regular expression");return h+""};
|
||||
$jscomp.polyfill("String.prototype.startsWith",function(h){return h?h:function(n,k){var p=$jscomp.checkStringArgs(this,n,"startsWith");n+="";var l=p.length,y=n.length;k=Math.max(0,Math.min(k|0,p.length));for(var f=0;f<y&&k<l;)if(p[k++]!=n[f++])return!1;return f>=y}},"es6","es3");
|
||||
$jscomp.polyfill("Array.prototype.copyWithin",function(h){function n(k){k=Number(k);return Infinity===k||-Infinity===k?k:k|0}return h?h:function(k,p,l){var y=this.length;k=n(k);p=n(p);l=void 0===l?y:n(l);k=0>k?Math.max(y+k,0):Math.min(k,y);p=0>p?Math.max(y+p,0):Math.min(p,y);l=0>l?Math.max(y+l,0):Math.min(l,y);if(k<p)for(;p<l;)p in this?this[k++]=this[p++]:(delete this[k++],p++);else for(l=Math.min(l,y+p-k),k+=l-p;l>p;)--l in this?this[--k]=this[l]:delete this[--k];return this}},"es6","es3");
|
||||
$jscomp.typedArrayCopyWithin=function(h){return h?h:Array.prototype.copyWithin};$jscomp.polyfill("Int8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8ClampedArray.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");
|
||||
$jscomp.polyfill("Uint16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float64Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");
|
||||
var DracoDecoderModule=function(){var h="undefined"!==typeof document&&document.currentScript?document.currentScript.src:void 0;"undefined"!==typeof __filename&&(h=h||__filename);return function(n){function k(e){return a.locateFile?a.locateFile(e,U):U+e}function p(e,b){if(e){var c=ia;var d=e+b;for(b=e;c[b]&&!(b>=d);)++b;if(16<b-e&&c.buffer&&ra)c=ra.decode(c.subarray(e,b));else{for(d="";e<b;){var g=c[e++];if(g&128){var t=c[e++]&63;if(192==(g&224))d+=String.fromCharCode((g&31)<<6|t);else{var aa=c[e++]&
|
||||
63;g=224==(g&240)?(g&15)<<12|t<<6|aa:(g&7)<<18|t<<12|aa<<6|c[e++]&63;65536>g?d+=String.fromCharCode(g):(g-=65536,d+=String.fromCharCode(55296|g>>10,56320|g&1023))}}else d+=String.fromCharCode(g)}c=d}}else c="";return c}function l(){var e=ja.buffer;a.HEAP8=W=new Int8Array(e);a.HEAP16=new Int16Array(e);a.HEAP32=ca=new Int32Array(e);a.HEAPU8=ia=new Uint8Array(e);a.HEAPU16=new Uint16Array(e);a.HEAPU32=Y=new Uint32Array(e);a.HEAPF32=new Float32Array(e);a.HEAPF64=new Float64Array(e)}function y(e){if(a.onAbort)a.onAbort(e);
|
||||
e="Aborted("+e+")";da(e);sa=!0;e=new WebAssembly.RuntimeError(e+". Build with -sASSERTIONS for more info.");ka(e);throw e;}function f(e){try{if(e==P&&ea)return new Uint8Array(ea);if(ma)return ma(e);throw"both async and sync fetching of the wasm failed";}catch(b){y(b)}}function q(){if(!ea&&(ta||fa)){if("function"==typeof fetch&&!P.startsWith("file://"))return fetch(P,{credentials:"same-origin"}).then(function(e){if(!e.ok)throw"failed to load wasm binary file at '"+P+"'";return e.arrayBuffer()}).catch(function(){return f(P)});
|
||||
if(na)return new Promise(function(e,b){na(P,function(c){e(new Uint8Array(c))},b)})}return Promise.resolve().then(function(){return f(P)})}function u(e){for(;0<e.length;)e.shift()(a)}function A(e){this.excPtr=e;this.ptr=e-24;this.set_type=function(b){Y[this.ptr+4>>2]=b};this.get_type=function(){return Y[this.ptr+4>>2]};this.set_destructor=function(b){Y[this.ptr+8>>2]=b};this.get_destructor=function(){return Y[this.ptr+8>>2]};this.set_refcount=function(b){ca[this.ptr>>2]=b};this.set_caught=function(b){W[this.ptr+
|
||||
12>>0]=b?1:0};this.get_caught=function(){return 0!=W[this.ptr+12>>0]};this.set_rethrown=function(b){W[this.ptr+13>>0]=b?1:0};this.get_rethrown=function(){return 0!=W[this.ptr+13>>0]};this.init=function(b,c){this.set_adjusted_ptr(0);this.set_type(b);this.set_destructor(c);this.set_refcount(0);this.set_caught(!1);this.set_rethrown(!1)};this.add_ref=function(){ca[this.ptr>>2]+=1};this.release_ref=function(){var b=ca[this.ptr>>2];ca[this.ptr>>2]=b-1;return 1===b};this.set_adjusted_ptr=function(b){Y[this.ptr+
|
||||
16>>2]=b};this.get_adjusted_ptr=function(){return Y[this.ptr+16>>2]};this.get_exception_ptr=function(){if(ua(this.get_type()))return Y[this.excPtr>>2];var b=this.get_adjusted_ptr();return 0!==b?b:this.excPtr}}function F(){function e(){if(!la&&(la=!0,a.calledRun=!0,!sa)){va=!0;u(oa);wa(a);if(a.onRuntimeInitialized)a.onRuntimeInitialized();if(a.postRun)for("function"==typeof a.postRun&&(a.postRun=[a.postRun]);a.postRun.length;)xa.unshift(a.postRun.shift());u(xa)}}if(!(0<ba)){if(a.preRun)for("function"==
|
||||
typeof a.preRun&&(a.preRun=[a.preRun]);a.preRun.length;)ya.unshift(a.preRun.shift());u(ya);0<ba||(a.setStatus?(a.setStatus("Running..."),setTimeout(function(){setTimeout(function(){a.setStatus("")},1);e()},1)):e())}}function v(){}function w(e){return(e||v).__cache__}function B(e,b){var c=w(b),d=c[e];if(d)return d;d=Object.create((b||v).prototype);d.ptr=e;return c[e]=d}function R(e){if("string"===typeof e){for(var b=0,c=0;c<e.length;++c){var d=e.charCodeAt(c);127>=d?b++:2047>=d?b+=2:55296<=d&&57343>=
|
||||
d?(b+=4,++c):b+=3}b=Array(b+1);c=0;d=b.length;if(0<d){d=c+d-1;for(var g=0;g<e.length;++g){var t=e.charCodeAt(g);if(55296<=t&&57343>=t){var aa=e.charCodeAt(++g);t=65536+((t&1023)<<10)|aa&1023}if(127>=t){if(c>=d)break;b[c++]=t}else{if(2047>=t){if(c+1>=d)break;b[c++]=192|t>>6}else{if(65535>=t){if(c+2>=d)break;b[c++]=224|t>>12}else{if(c+3>=d)break;b[c++]=240|t>>18;b[c++]=128|t>>12&63}b[c++]=128|t>>6&63}b[c++]=128|t&63}}b[c]=0}e=r.alloc(b,W);r.copy(b,W,e);return e}return e}function Z(e){if("object"===
|
||||
typeof e){var b=r.alloc(e,W);r.copy(e,W,b);return b}return e}function X(){throw"cannot construct a VoidPtr, no constructor in IDL";}function S(){this.ptr=za();w(S)[this.ptr]=this}function Q(){this.ptr=Aa();w(Q)[this.ptr]=this}function V(){this.ptr=Ba();w(V)[this.ptr]=this}function x(){this.ptr=Ca();w(x)[this.ptr]=this}function D(){this.ptr=Da();w(D)[this.ptr]=this}function G(){this.ptr=Ea();w(G)[this.ptr]=this}function H(){this.ptr=Fa();w(H)[this.ptr]=this}function E(){this.ptr=Ga();w(E)[this.ptr]=
|
||||
this}function T(){this.ptr=Ha();w(T)[this.ptr]=this}function C(){throw"cannot construct a Status, no constructor in IDL";}function I(){this.ptr=Ia();w(I)[this.ptr]=this}function J(){this.ptr=Ja();w(J)[this.ptr]=this}function K(){this.ptr=Ka();w(K)[this.ptr]=this}function L(){this.ptr=La();w(L)[this.ptr]=this}function M(){this.ptr=Ma();w(M)[this.ptr]=this}function N(){this.ptr=Na();w(N)[this.ptr]=this}function O(){this.ptr=Oa();w(O)[this.ptr]=this}function z(){this.ptr=Pa();w(z)[this.ptr]=this}function m(){this.ptr=
|
||||
Qa();w(m)[this.ptr]=this}n=void 0===n?{}:n;var a="undefined"!=typeof n?n:{},wa,ka;a.ready=new Promise(function(e,b){wa=e;ka=b});var Ra=!1,Sa=!1;a.onRuntimeInitialized=function(){Ra=!0;if(Sa&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.onModuleParsed=function(){Sa=!0;if(Ra&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.isVersionSupported=function(e){if("string"!==typeof e)return!1;e=e.split(".");return 2>e.length||3<e.length?!1:1==e[0]&&0<=e[1]&&5>=e[1]?!0:0!=e[0]||10<
|
||||
e[1]?!1:!0};var Ta=Object.assign({},a),ta="object"==typeof window,fa="function"==typeof importScripts,Ua="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,U="";if(Ua){var Va=require("fs"),pa=require("path");U=fa?pa.dirname(U)+"/":__dirname+"/";var Wa=function(e,b){e=e.startsWith("file://")?new URL(e):pa.normalize(e);return Va.readFileSync(e,b?void 0:"utf8")};var ma=function(e){e=Wa(e,!0);e.buffer||(e=new Uint8Array(e));return e};var na=function(e,
|
||||
b,c){e=e.startsWith("file://")?new URL(e):pa.normalize(e);Va.readFile(e,function(d,g){d?c(d):b(g.buffer)})};1<process.argv.length&&process.argv[1].replace(/\\/g,"/");process.argv.slice(2);a.inspect=function(){return"[Emscripten Module object]"}}else if(ta||fa)fa?U=self.location.href:"undefined"!=typeof document&&document.currentScript&&(U=document.currentScript.src),h&&(U=h),U=0!==U.indexOf("blob:")?U.substr(0,U.replace(/[?#].*/,"").lastIndexOf("/")+1):"",Wa=function(e){var b=new XMLHttpRequest;b.open("GET",
|
||||
e,!1);b.send(null);return b.responseText},fa&&(ma=function(e){var b=new XMLHttpRequest;b.open("GET",e,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)}),na=function(e,b,c){var d=new XMLHttpRequest;d.open("GET",e,!0);d.responseType="arraybuffer";d.onload=function(){200==d.status||0==d.status&&d.response?b(d.response):c()};d.onerror=c;d.send(null)};a.print||console.log.bind(console);var da=a.printErr||console.warn.bind(console);Object.assign(a,Ta);Ta=null;var ea;a.wasmBinary&&
|
||||
(ea=a.wasmBinary);"object"!=typeof WebAssembly&&y("no native wasm support detected");var ja,sa=!1,ra="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0,W,ia,ca,Y,ya=[],oa=[],xa=[],va=!1,ba=0,qa=null,ha=null;var P="draco_decoder_gltf.wasm";P.startsWith("data:application/octet-stream;base64,")||(P=k(P));var pd=0,qd={b:function(e,b,c){(new A(e)).init(b,c);pd++;throw e;},a:function(){y("")},d:function(e,b,c){ia.copyWithin(e,b,b+c)},c:function(e){var b=ia.length;e>>>=0;if(2147483648<e)return!1;
|
||||
for(var c=1;4>=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,e+100663296);var g=Math;d=Math.max(e,d);g=g.min.call(g,2147483648,d+(65536-d%65536)%65536);a:{d=ja.buffer;try{ja.grow(g-d.byteLength+65535>>>16);l();var t=1;break a}catch(aa){}t=void 0}if(t)return!0}return!1}};(function(){function e(g,t){a.asm=g.exports;ja=a.asm.e;l();oa.unshift(a.asm.f);ba--;a.monitorRunDependencies&&a.monitorRunDependencies(ba);0==ba&&(null!==qa&&(clearInterval(qa),qa=null),ha&&(g=ha,ha=null,g()))}function b(g){e(g.instance)}
|
||||
function c(g){return q().then(function(t){return WebAssembly.instantiate(t,d)}).then(function(t){return t}).then(g,function(t){da("failed to asynchronously prepare wasm: "+t);y(t)})}var d={a:qd};ba++;a.monitorRunDependencies&&a.monitorRunDependencies(ba);if(a.instantiateWasm)try{return a.instantiateWasm(d,e)}catch(g){da("Module.instantiateWasm callback failed with error: "+g),ka(g)}(function(){return ea||"function"!=typeof WebAssembly.instantiateStreaming||P.startsWith("data:application/octet-stream;base64,")||
|
||||
P.startsWith("file://")||Ua||"function"!=typeof fetch?c(b):fetch(P,{credentials:"same-origin"}).then(function(g){return WebAssembly.instantiateStreaming(g,d).then(b,function(t){da("wasm streaming compile failed: "+t);da("falling back to ArrayBuffer instantiation");return c(b)})})})().catch(ka);return{}})();var Xa=a._emscripten_bind_VoidPtr___destroy___0=function(){return(Xa=a._emscripten_bind_VoidPtr___destroy___0=a.asm.h).apply(null,arguments)},za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=
|
||||
function(){return(za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=a.asm.i).apply(null,arguments)},Ya=a._emscripten_bind_DecoderBuffer_Init_2=function(){return(Ya=a._emscripten_bind_DecoderBuffer_Init_2=a.asm.j).apply(null,arguments)},Za=a._emscripten_bind_DecoderBuffer___destroy___0=function(){return(Za=a._emscripten_bind_DecoderBuffer___destroy___0=a.asm.k).apply(null,arguments)},Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=function(){return(Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=
|
||||
a.asm.l).apply(null,arguments)},$a=a._emscripten_bind_AttributeTransformData_transform_type_0=function(){return($a=a._emscripten_bind_AttributeTransformData_transform_type_0=a.asm.m).apply(null,arguments)},ab=a._emscripten_bind_AttributeTransformData___destroy___0=function(){return(ab=a._emscripten_bind_AttributeTransformData___destroy___0=a.asm.n).apply(null,arguments)},Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=function(){return(Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=
|
||||
a.asm.o).apply(null,arguments)},bb=a._emscripten_bind_GeometryAttribute___destroy___0=function(){return(bb=a._emscripten_bind_GeometryAttribute___destroy___0=a.asm.p).apply(null,arguments)},Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=function(){return(Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=a.asm.q).apply(null,arguments)},cb=a._emscripten_bind_PointAttribute_size_0=function(){return(cb=a._emscripten_bind_PointAttribute_size_0=a.asm.r).apply(null,arguments)},db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=
|
||||
function(){return(db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=a.asm.s).apply(null,arguments)},eb=a._emscripten_bind_PointAttribute_attribute_type_0=function(){return(eb=a._emscripten_bind_PointAttribute_attribute_type_0=a.asm.t).apply(null,arguments)},fb=a._emscripten_bind_PointAttribute_data_type_0=function(){return(fb=a._emscripten_bind_PointAttribute_data_type_0=a.asm.u).apply(null,arguments)},gb=a._emscripten_bind_PointAttribute_num_components_0=function(){return(gb=a._emscripten_bind_PointAttribute_num_components_0=
|
||||
a.asm.v).apply(null,arguments)},hb=a._emscripten_bind_PointAttribute_normalized_0=function(){return(hb=a._emscripten_bind_PointAttribute_normalized_0=a.asm.w).apply(null,arguments)},ib=a._emscripten_bind_PointAttribute_byte_stride_0=function(){return(ib=a._emscripten_bind_PointAttribute_byte_stride_0=a.asm.x).apply(null,arguments)},jb=a._emscripten_bind_PointAttribute_byte_offset_0=function(){return(jb=a._emscripten_bind_PointAttribute_byte_offset_0=a.asm.y).apply(null,arguments)},kb=a._emscripten_bind_PointAttribute_unique_id_0=
|
||||
function(){return(kb=a._emscripten_bind_PointAttribute_unique_id_0=a.asm.z).apply(null,arguments)},lb=a._emscripten_bind_PointAttribute___destroy___0=function(){return(lb=a._emscripten_bind_PointAttribute___destroy___0=a.asm.A).apply(null,arguments)},Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=function(){return(Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=a.asm.B).apply(null,arguments)},mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=
|
||||
function(){return(mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=a.asm.C).apply(null,arguments)},nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=function(){return(nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=a.asm.D).apply(null,arguments)},ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=function(){return(ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=a.asm.E).apply(null,arguments)},pb=
|
||||
a._emscripten_bind_AttributeQuantizationTransform_range_0=function(){return(pb=a._emscripten_bind_AttributeQuantizationTransform_range_0=a.asm.F).apply(null,arguments)},qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=function(){return(qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=a.asm.G).apply(null,arguments)},Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=function(){return(Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=
|
||||
a.asm.H).apply(null,arguments)},rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=function(){return(rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=a.asm.I).apply(null,arguments)},sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=function(){return(sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=a.asm.J).apply(null,arguments)},tb=a._emscripten_bind_AttributeOctahedronTransform___destroy___0=function(){return(tb=
|
||||
a._emscripten_bind_AttributeOctahedronTransform___destroy___0=a.asm.K).apply(null,arguments)},Fa=a._emscripten_bind_PointCloud_PointCloud_0=function(){return(Fa=a._emscripten_bind_PointCloud_PointCloud_0=a.asm.L).apply(null,arguments)},ub=a._emscripten_bind_PointCloud_num_attributes_0=function(){return(ub=a._emscripten_bind_PointCloud_num_attributes_0=a.asm.M).apply(null,arguments)},vb=a._emscripten_bind_PointCloud_num_points_0=function(){return(vb=a._emscripten_bind_PointCloud_num_points_0=a.asm.N).apply(null,
|
||||
arguments)},wb=a._emscripten_bind_PointCloud___destroy___0=function(){return(wb=a._emscripten_bind_PointCloud___destroy___0=a.asm.O).apply(null,arguments)},Ga=a._emscripten_bind_Mesh_Mesh_0=function(){return(Ga=a._emscripten_bind_Mesh_Mesh_0=a.asm.P).apply(null,arguments)},xb=a._emscripten_bind_Mesh_num_faces_0=function(){return(xb=a._emscripten_bind_Mesh_num_faces_0=a.asm.Q).apply(null,arguments)},yb=a._emscripten_bind_Mesh_num_attributes_0=function(){return(yb=a._emscripten_bind_Mesh_num_attributes_0=
|
||||
a.asm.R).apply(null,arguments)},zb=a._emscripten_bind_Mesh_num_points_0=function(){return(zb=a._emscripten_bind_Mesh_num_points_0=a.asm.S).apply(null,arguments)},Ab=a._emscripten_bind_Mesh___destroy___0=function(){return(Ab=a._emscripten_bind_Mesh___destroy___0=a.asm.T).apply(null,arguments)},Ha=a._emscripten_bind_Metadata_Metadata_0=function(){return(Ha=a._emscripten_bind_Metadata_Metadata_0=a.asm.U).apply(null,arguments)},Bb=a._emscripten_bind_Metadata___destroy___0=function(){return(Bb=a._emscripten_bind_Metadata___destroy___0=
|
||||
a.asm.V).apply(null,arguments)},Cb=a._emscripten_bind_Status_code_0=function(){return(Cb=a._emscripten_bind_Status_code_0=a.asm.W).apply(null,arguments)},Db=a._emscripten_bind_Status_ok_0=function(){return(Db=a._emscripten_bind_Status_ok_0=a.asm.X).apply(null,arguments)},Eb=a._emscripten_bind_Status_error_msg_0=function(){return(Eb=a._emscripten_bind_Status_error_msg_0=a.asm.Y).apply(null,arguments)},Fb=a._emscripten_bind_Status___destroy___0=function(){return(Fb=a._emscripten_bind_Status___destroy___0=
|
||||
a.asm.Z).apply(null,arguments)},Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=function(){return(Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=a.asm._).apply(null,arguments)},Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=function(){return(Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=a.asm.$).apply(null,arguments)},Hb=a._emscripten_bind_DracoFloat32Array_size_0=function(){return(Hb=a._emscripten_bind_DracoFloat32Array_size_0=a.asm.aa).apply(null,arguments)},Ib=
|
||||
a._emscripten_bind_DracoFloat32Array___destroy___0=function(){return(Ib=a._emscripten_bind_DracoFloat32Array___destroy___0=a.asm.ba).apply(null,arguments)},Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=function(){return(Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=a.asm.ca).apply(null,arguments)},Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=function(){return(Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=a.asm.da).apply(null,arguments)},Kb=a._emscripten_bind_DracoInt8Array_size_0=
|
||||
function(){return(Kb=a._emscripten_bind_DracoInt8Array_size_0=a.asm.ea).apply(null,arguments)},Lb=a._emscripten_bind_DracoInt8Array___destroy___0=function(){return(Lb=a._emscripten_bind_DracoInt8Array___destroy___0=a.asm.fa).apply(null,arguments)},Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=function(){return(Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=a.asm.ga).apply(null,arguments)},Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=function(){return(Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=
|
||||
a.asm.ha).apply(null,arguments)},Nb=a._emscripten_bind_DracoUInt8Array_size_0=function(){return(Nb=a._emscripten_bind_DracoUInt8Array_size_0=a.asm.ia).apply(null,arguments)},Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=function(){return(Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=a.asm.ja).apply(null,arguments)},La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=function(){return(La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=a.asm.ka).apply(null,arguments)},Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=
|
||||
function(){return(Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=a.asm.la).apply(null,arguments)},Qb=a._emscripten_bind_DracoInt16Array_size_0=function(){return(Qb=a._emscripten_bind_DracoInt16Array_size_0=a.asm.ma).apply(null,arguments)},Rb=a._emscripten_bind_DracoInt16Array___destroy___0=function(){return(Rb=a._emscripten_bind_DracoInt16Array___destroy___0=a.asm.na).apply(null,arguments)},Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=function(){return(Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=
|
||||
a.asm.oa).apply(null,arguments)},Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=function(){return(Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=a.asm.pa).apply(null,arguments)},Tb=a._emscripten_bind_DracoUInt16Array_size_0=function(){return(Tb=a._emscripten_bind_DracoUInt16Array_size_0=a.asm.qa).apply(null,arguments)},Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=function(){return(Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=a.asm.ra).apply(null,arguments)},Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=
|
||||
function(){return(Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=a.asm.sa).apply(null,arguments)},Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=function(){return(Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=a.asm.ta).apply(null,arguments)},Wb=a._emscripten_bind_DracoInt32Array_size_0=function(){return(Wb=a._emscripten_bind_DracoInt32Array_size_0=a.asm.ua).apply(null,arguments)},Xb=a._emscripten_bind_DracoInt32Array___destroy___0=function(){return(Xb=a._emscripten_bind_DracoInt32Array___destroy___0=
|
||||
a.asm.va).apply(null,arguments)},Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=function(){return(Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=a.asm.wa).apply(null,arguments)},Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=function(){return(Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=a.asm.xa).apply(null,arguments)},Zb=a._emscripten_bind_DracoUInt32Array_size_0=function(){return(Zb=a._emscripten_bind_DracoUInt32Array_size_0=a.asm.ya).apply(null,arguments)},$b=a._emscripten_bind_DracoUInt32Array___destroy___0=
|
||||
function(){return($b=a._emscripten_bind_DracoUInt32Array___destroy___0=a.asm.za).apply(null,arguments)},Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=function(){return(Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=a.asm.Aa).apply(null,arguments)},ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=function(){return(ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=a.asm.Ba).apply(null,arguments)},bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=function(){return(bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=
|
||||
a.asm.Ca).apply(null,arguments)},cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=function(){return(cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=a.asm.Da).apply(null,arguments)},dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=function(){return(dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=a.asm.Ea).apply(null,arguments)},ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=function(){return(ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=a.asm.Fa).apply(null,
|
||||
arguments)},fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=function(){return(fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=a.asm.Ga).apply(null,arguments)},gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=function(){return(gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=a.asm.Ha).apply(null,arguments)},hc=a._emscripten_bind_MetadataQuerier___destroy___0=function(){return(hc=a._emscripten_bind_MetadataQuerier___destroy___0=a.asm.Ia).apply(null,arguments)},Qa=a._emscripten_bind_Decoder_Decoder_0=
|
||||
function(){return(Qa=a._emscripten_bind_Decoder_Decoder_0=a.asm.Ja).apply(null,arguments)},ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=function(){return(ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=a.asm.Ka).apply(null,arguments)},jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=function(){return(jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=a.asm.La).apply(null,arguments)},kc=a._emscripten_bind_Decoder_GetAttributeId_2=function(){return(kc=a._emscripten_bind_Decoder_GetAttributeId_2=
|
||||
a.asm.Ma).apply(null,arguments)},lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=function(){return(lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=a.asm.Na).apply(null,arguments)},mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=function(){return(mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=a.asm.Oa).apply(null,arguments)},nc=a._emscripten_bind_Decoder_GetAttribute_2=function(){return(nc=a._emscripten_bind_Decoder_GetAttribute_2=a.asm.Pa).apply(null,arguments)},
|
||||
oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=function(){return(oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=a.asm.Qa).apply(null,arguments)},pc=a._emscripten_bind_Decoder_GetMetadata_1=function(){return(pc=a._emscripten_bind_Decoder_GetMetadata_1=a.asm.Ra).apply(null,arguments)},qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=function(){return(qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=a.asm.Sa).apply(null,arguments)},rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=
|
||||
function(){return(rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=a.asm.Ta).apply(null,arguments)},sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=function(){return(sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=a.asm.Ua).apply(null,arguments)},tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=function(){return(tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=a.asm.Va).apply(null,arguments)},uc=a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=function(){return(uc=
|
||||
a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=a.asm.Wa).apply(null,arguments)},vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=function(){return(vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=a.asm.Xa).apply(null,arguments)},wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=function(){return(wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=a.asm.Ya).apply(null,arguments)},xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=function(){return(xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=
|
||||
a.asm.Za).apply(null,arguments)},yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=function(){return(yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=a.asm._a).apply(null,arguments)},zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=function(){return(zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=a.asm.$a).apply(null,arguments)},Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=function(){return(Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=
|
||||
a.asm.ab).apply(null,arguments)},Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=function(){return(Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=a.asm.bb).apply(null,arguments)},Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=function(){return(Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=a.asm.cb).apply(null,arguments)},Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=function(){return(Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=
|
||||
a.asm.db).apply(null,arguments)},Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=function(){return(Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=a.asm.eb).apply(null,arguments)},Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=function(){return(Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=a.asm.fb).apply(null,arguments)},Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=function(){return(Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=
|
||||
a.asm.gb).apply(null,arguments)},Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=function(){return(Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=a.asm.hb).apply(null,arguments)},Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=function(){return(Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=a.asm.ib).apply(null,arguments)},Jc=a._emscripten_bind_Decoder___destroy___0=function(){return(Jc=a._emscripten_bind_Decoder___destroy___0=a.asm.jb).apply(null,arguments)},Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=
|
||||
function(){return(Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=a.asm.kb).apply(null,arguments)},Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=function(){return(Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=a.asm.lb).apply(null,arguments)},Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=function(){return(Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=
|
||||
a.asm.mb).apply(null,arguments)},Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=function(){return(Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=a.asm.nb).apply(null,arguments)},Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=function(){return(Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=a.asm.ob).apply(null,arguments)},Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=function(){return(Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=
|
||||
a.asm.pb).apply(null,arguments)},Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=function(){return(Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=a.asm.qb).apply(null,arguments)},Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=function(){return(Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=a.asm.rb).apply(null,arguments)},Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=function(){return(Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=
|
||||
a.asm.sb).apply(null,arguments)},Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=function(){return(Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=a.asm.tb).apply(null,arguments)},Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=function(){return(Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=a.asm.ub).apply(null,arguments)},Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=function(){return(Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=
|
||||
a.asm.vb).apply(null,arguments)},Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=function(){return(Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=a.asm.wb).apply(null,arguments)},Xc=a._emscripten_enum_draco_DataType_DT_INVALID=function(){return(Xc=a._emscripten_enum_draco_DataType_DT_INVALID=a.asm.xb).apply(null,arguments)},Yc=a._emscripten_enum_draco_DataType_DT_INT8=function(){return(Yc=a._emscripten_enum_draco_DataType_DT_INT8=a.asm.yb).apply(null,arguments)},Zc=
|
||||
a._emscripten_enum_draco_DataType_DT_UINT8=function(){return(Zc=a._emscripten_enum_draco_DataType_DT_UINT8=a.asm.zb).apply(null,arguments)},$c=a._emscripten_enum_draco_DataType_DT_INT16=function(){return($c=a._emscripten_enum_draco_DataType_DT_INT16=a.asm.Ab).apply(null,arguments)},ad=a._emscripten_enum_draco_DataType_DT_UINT16=function(){return(ad=a._emscripten_enum_draco_DataType_DT_UINT16=a.asm.Bb).apply(null,arguments)},bd=a._emscripten_enum_draco_DataType_DT_INT32=function(){return(bd=a._emscripten_enum_draco_DataType_DT_INT32=
|
||||
a.asm.Cb).apply(null,arguments)},cd=a._emscripten_enum_draco_DataType_DT_UINT32=function(){return(cd=a._emscripten_enum_draco_DataType_DT_UINT32=a.asm.Db).apply(null,arguments)},dd=a._emscripten_enum_draco_DataType_DT_INT64=function(){return(dd=a._emscripten_enum_draco_DataType_DT_INT64=a.asm.Eb).apply(null,arguments)},ed=a._emscripten_enum_draco_DataType_DT_UINT64=function(){return(ed=a._emscripten_enum_draco_DataType_DT_UINT64=a.asm.Fb).apply(null,arguments)},fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=
|
||||
function(){return(fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=a.asm.Gb).apply(null,arguments)},gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=function(){return(gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=a.asm.Hb).apply(null,arguments)},hd=a._emscripten_enum_draco_DataType_DT_BOOL=function(){return(hd=a._emscripten_enum_draco_DataType_DT_BOOL=a.asm.Ib).apply(null,arguments)},id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=function(){return(id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=
|
||||
a.asm.Jb).apply(null,arguments)},jd=a._emscripten_enum_draco_StatusCode_OK=function(){return(jd=a._emscripten_enum_draco_StatusCode_OK=a.asm.Kb).apply(null,arguments)},kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=function(){return(kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=a.asm.Lb).apply(null,arguments)},ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=function(){return(ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=a.asm.Mb).apply(null,arguments)},md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=
|
||||
function(){return(md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=a.asm.Nb).apply(null,arguments)},nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=function(){return(nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=a.asm.Ob).apply(null,arguments)},od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=function(){return(od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=a.asm.Pb).apply(null,arguments)};a._malloc=function(){return(a._malloc=a.asm.Qb).apply(null,arguments)};
|
||||
a._free=function(){return(a._free=a.asm.Rb).apply(null,arguments)};var ua=function(){return(ua=a.asm.Sb).apply(null,arguments)};a.___start_em_js=11660;a.___stop_em_js=11758;var la;ha=function b(){la||F();la||(ha=b)};if(a.preInit)for("function"==typeof a.preInit&&(a.preInit=[a.preInit]);0<a.preInit.length;)a.preInit.pop()();F();v.prototype=Object.create(v.prototype);v.prototype.constructor=v;v.prototype.__class__=v;v.__cache__={};a.WrapperObject=v;a.getCache=w;a.wrapPointer=B;a.castObject=function(b,
|
||||
c){return B(b.ptr,c)};a.NULL=B(0);a.destroy=function(b){if(!b.__destroy__)throw"Error: Cannot destroy object. (Did you create it yourself?)";b.__destroy__();delete w(b.__class__)[b.ptr]};a.compare=function(b,c){return b.ptr===c.ptr};a.getPointer=function(b){return b.ptr};a.getClass=function(b){return b.__class__};var r={buffer:0,size:0,pos:0,temps:[],needed:0,prepare:function(){if(r.needed){for(var b=0;b<r.temps.length;b++)a._free(r.temps[b]);r.temps.length=0;a._free(r.buffer);r.buffer=0;r.size+=
|
||||
r.needed;r.needed=0}r.buffer||(r.size+=128,r.buffer=a._malloc(r.size),r.buffer||y(void 0));r.pos=0},alloc:function(b,c){r.buffer||y(void 0);b=b.length*c.BYTES_PER_ELEMENT;b=b+7&-8;r.pos+b>=r.size?(0<b||y(void 0),r.needed+=b,c=a._malloc(b),r.temps.push(c)):(c=r.buffer+r.pos,r.pos+=b);return c},copy:function(b,c,d){d>>>=0;switch(c.BYTES_PER_ELEMENT){case 2:d>>>=1;break;case 4:d>>>=2;break;case 8:d>>>=3}for(var g=0;g<b.length;g++)c[d+g]=b[g]}};X.prototype=Object.create(v.prototype);X.prototype.constructor=
|
||||
X;X.prototype.__class__=X;X.__cache__={};a.VoidPtr=X;X.prototype.__destroy__=X.prototype.__destroy__=function(){Xa(this.ptr)};S.prototype=Object.create(v.prototype);S.prototype.constructor=S;S.prototype.__class__=S;S.__cache__={};a.DecoderBuffer=S;S.prototype.Init=S.prototype.Init=function(b,c){var d=this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);Ya(d,b,c)};S.prototype.__destroy__=S.prototype.__destroy__=function(){Za(this.ptr)};Q.prototype=Object.create(v.prototype);
|
||||
Q.prototype.constructor=Q;Q.prototype.__class__=Q;Q.__cache__={};a.AttributeTransformData=Q;Q.prototype.transform_type=Q.prototype.transform_type=function(){return $a(this.ptr)};Q.prototype.__destroy__=Q.prototype.__destroy__=function(){ab(this.ptr)};V.prototype=Object.create(v.prototype);V.prototype.constructor=V;V.prototype.__class__=V;V.__cache__={};a.GeometryAttribute=V;V.prototype.__destroy__=V.prototype.__destroy__=function(){bb(this.ptr)};x.prototype=Object.create(v.prototype);x.prototype.constructor=
|
||||
x;x.prototype.__class__=x;x.__cache__={};a.PointAttribute=x;x.prototype.size=x.prototype.size=function(){return cb(this.ptr)};x.prototype.GetAttributeTransformData=x.prototype.GetAttributeTransformData=function(){return B(db(this.ptr),Q)};x.prototype.attribute_type=x.prototype.attribute_type=function(){return eb(this.ptr)};x.prototype.data_type=x.prototype.data_type=function(){return fb(this.ptr)};x.prototype.num_components=x.prototype.num_components=function(){return gb(this.ptr)};x.prototype.normalized=
|
||||
x.prototype.normalized=function(){return!!hb(this.ptr)};x.prototype.byte_stride=x.prototype.byte_stride=function(){return ib(this.ptr)};x.prototype.byte_offset=x.prototype.byte_offset=function(){return jb(this.ptr)};x.prototype.unique_id=x.prototype.unique_id=function(){return kb(this.ptr)};x.prototype.__destroy__=x.prototype.__destroy__=function(){lb(this.ptr)};D.prototype=Object.create(v.prototype);D.prototype.constructor=D;D.prototype.__class__=D;D.__cache__={};a.AttributeQuantizationTransform=
|
||||
D;D.prototype.InitFromAttribute=D.prototype.InitFromAttribute=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return!!mb(c,b)};D.prototype.quantization_bits=D.prototype.quantization_bits=function(){return nb(this.ptr)};D.prototype.min_value=D.prototype.min_value=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return ob(c,b)};D.prototype.range=D.prototype.range=function(){return pb(this.ptr)};D.prototype.__destroy__=D.prototype.__destroy__=function(){qb(this.ptr)};G.prototype=
|
||||
Object.create(v.prototype);G.prototype.constructor=G;G.prototype.__class__=G;G.__cache__={};a.AttributeOctahedronTransform=G;G.prototype.InitFromAttribute=G.prototype.InitFromAttribute=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return!!rb(c,b)};G.prototype.quantization_bits=G.prototype.quantization_bits=function(){return sb(this.ptr)};G.prototype.__destroy__=G.prototype.__destroy__=function(){tb(this.ptr)};H.prototype=Object.create(v.prototype);H.prototype.constructor=H;H.prototype.__class__=
|
||||
H;H.__cache__={};a.PointCloud=H;H.prototype.num_attributes=H.prototype.num_attributes=function(){return ub(this.ptr)};H.prototype.num_points=H.prototype.num_points=function(){return vb(this.ptr)};H.prototype.__destroy__=H.prototype.__destroy__=function(){wb(this.ptr)};E.prototype=Object.create(v.prototype);E.prototype.constructor=E;E.prototype.__class__=E;E.__cache__={};a.Mesh=E;E.prototype.num_faces=E.prototype.num_faces=function(){return xb(this.ptr)};E.prototype.num_attributes=E.prototype.num_attributes=
|
||||
function(){return yb(this.ptr)};E.prototype.num_points=E.prototype.num_points=function(){return zb(this.ptr)};E.prototype.__destroy__=E.prototype.__destroy__=function(){Ab(this.ptr)};T.prototype=Object.create(v.prototype);T.prototype.constructor=T;T.prototype.__class__=T;T.__cache__={};a.Metadata=T;T.prototype.__destroy__=T.prototype.__destroy__=function(){Bb(this.ptr)};C.prototype=Object.create(v.prototype);C.prototype.constructor=C;C.prototype.__class__=C;C.__cache__={};a.Status=C;C.prototype.code=
|
||||
C.prototype.code=function(){return Cb(this.ptr)};C.prototype.ok=C.prototype.ok=function(){return!!Db(this.ptr)};C.prototype.error_msg=C.prototype.error_msg=function(){return p(Eb(this.ptr))};C.prototype.__destroy__=C.prototype.__destroy__=function(){Fb(this.ptr)};I.prototype=Object.create(v.prototype);I.prototype.constructor=I;I.prototype.__class__=I;I.__cache__={};a.DracoFloat32Array=I;I.prototype.GetValue=I.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Gb(c,
|
||||
b)};I.prototype.size=I.prototype.size=function(){return Hb(this.ptr)};I.prototype.__destroy__=I.prototype.__destroy__=function(){Ib(this.ptr)};J.prototype=Object.create(v.prototype);J.prototype.constructor=J;J.prototype.__class__=J;J.__cache__={};a.DracoInt8Array=J;J.prototype.GetValue=J.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Jb(c,b)};J.prototype.size=J.prototype.size=function(){return Kb(this.ptr)};J.prototype.__destroy__=J.prototype.__destroy__=function(){Lb(this.ptr)};
|
||||
K.prototype=Object.create(v.prototype);K.prototype.constructor=K;K.prototype.__class__=K;K.__cache__={};a.DracoUInt8Array=K;K.prototype.GetValue=K.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Mb(c,b)};K.prototype.size=K.prototype.size=function(){return Nb(this.ptr)};K.prototype.__destroy__=K.prototype.__destroy__=function(){Ob(this.ptr)};L.prototype=Object.create(v.prototype);L.prototype.constructor=L;L.prototype.__class__=L;L.__cache__={};a.DracoInt16Array=
|
||||
L;L.prototype.GetValue=L.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Pb(c,b)};L.prototype.size=L.prototype.size=function(){return Qb(this.ptr)};L.prototype.__destroy__=L.prototype.__destroy__=function(){Rb(this.ptr)};M.prototype=Object.create(v.prototype);M.prototype.constructor=M;M.prototype.__class__=M;M.__cache__={};a.DracoUInt16Array=M;M.prototype.GetValue=M.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Sb(c,b)};
|
||||
M.prototype.size=M.prototype.size=function(){return Tb(this.ptr)};M.prototype.__destroy__=M.prototype.__destroy__=function(){Ub(this.ptr)};N.prototype=Object.create(v.prototype);N.prototype.constructor=N;N.prototype.__class__=N;N.__cache__={};a.DracoInt32Array=N;N.prototype.GetValue=N.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Vb(c,b)};N.prototype.size=N.prototype.size=function(){return Wb(this.ptr)};N.prototype.__destroy__=N.prototype.__destroy__=function(){Xb(this.ptr)};
|
||||
O.prototype=Object.create(v.prototype);O.prototype.constructor=O;O.prototype.__class__=O;O.__cache__={};a.DracoUInt32Array=O;O.prototype.GetValue=O.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Yb(c,b)};O.prototype.size=O.prototype.size=function(){return Zb(this.ptr)};O.prototype.__destroy__=O.prototype.__destroy__=function(){$b(this.ptr)};z.prototype=Object.create(v.prototype);z.prototype.constructor=z;z.prototype.__class__=z;z.__cache__={};a.MetadataQuerier=
|
||||
z;z.prototype.HasEntry=z.prototype.HasEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return!!ac(d,b,c)};z.prototype.GetIntEntry=z.prototype.GetIntEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return bc(d,b,c)};z.prototype.GetIntEntryArray=z.prototype.GetIntEntryArray=function(b,c,d){var g=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===
|
||||
typeof c?c.ptr:R(c);d&&"object"===typeof d&&(d=d.ptr);cc(g,b,c,d)};z.prototype.GetDoubleEntry=z.prototype.GetDoubleEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return dc(d,b,c)};z.prototype.GetStringEntry=z.prototype.GetStringEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return p(ec(d,b,c))};z.prototype.NumEntries=z.prototype.NumEntries=function(b){var c=this.ptr;
|
||||
b&&"object"===typeof b&&(b=b.ptr);return fc(c,b)};z.prototype.GetEntryName=z.prototype.GetEntryName=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return p(gc(d,b,c))};z.prototype.__destroy__=z.prototype.__destroy__=function(){hc(this.ptr)};m.prototype=Object.create(v.prototype);m.prototype.constructor=m;m.prototype.__class__=m;m.__cache__={};a.Decoder=m;m.prototype.DecodeArrayToPointCloud=m.prototype.DecodeArrayToPointCloud=function(b,c,d){var g=
|
||||
this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return B(ic(g,b,c,d),C)};m.prototype.DecodeArrayToMesh=m.prototype.DecodeArrayToMesh=function(b,c,d){var g=this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return B(jc(g,b,c,d),C)};m.prototype.GetAttributeId=m.prototype.GetAttributeId=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&
|
||||
(c=c.ptr);return kc(d,b,c)};m.prototype.GetAttributeIdByName=m.prototype.GetAttributeIdByName=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return lc(d,b,c)};m.prototype.GetAttributeIdByMetadataEntry=m.prototype.GetAttributeIdByMetadataEntry=function(b,c,d){var g=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);d=d&&"object"===typeof d?d.ptr:R(d);return mc(g,b,c,d)};m.prototype.GetAttribute=
|
||||
m.prototype.GetAttribute=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(nc(d,b,c),x)};m.prototype.GetAttributeByUniqueId=m.prototype.GetAttributeByUniqueId=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(oc(d,b,c),x)};m.prototype.GetMetadata=m.prototype.GetMetadata=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return B(pc(c,b),T)};m.prototype.GetAttributeMetadata=m.prototype.GetAttributeMetadata=
|
||||
function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(qc(d,b,c),T)};m.prototype.GetFaceFromMesh=m.prototype.GetFaceFromMesh=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!rc(g,b,c,d)};m.prototype.GetTriangleStripsFromMesh=m.prototype.GetTriangleStripsFromMesh=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);
|
||||
return sc(d,b,c)};m.prototype.GetTrianglesUInt16Array=m.prototype.GetTrianglesUInt16Array=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!tc(g,b,c,d)};m.prototype.GetTrianglesUInt32Array=m.prototype.GetTrianglesUInt32Array=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!uc(g,b,c,d)};m.prototype.GetAttributeFloat=m.prototype.GetAttributeFloat=
|
||||
function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!vc(g,b,c,d)};m.prototype.GetAttributeFloatForAllPoints=m.prototype.GetAttributeFloatForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!wc(g,b,c,d)};m.prototype.GetAttributeIntForAllPoints=m.prototype.GetAttributeIntForAllPoints=function(b,c,d){var g=this.ptr;
|
||||
b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!xc(g,b,c,d)};m.prototype.GetAttributeInt8ForAllPoints=m.prototype.GetAttributeInt8ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!yc(g,b,c,d)};m.prototype.GetAttributeUInt8ForAllPoints=m.prototype.GetAttributeUInt8ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=
|
||||
b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!zc(g,b,c,d)};m.prototype.GetAttributeInt16ForAllPoints=m.prototype.GetAttributeInt16ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Ac(g,b,c,d)};m.prototype.GetAttributeUInt16ForAllPoints=m.prototype.GetAttributeUInt16ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&
|
||||
(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Bc(g,b,c,d)};m.prototype.GetAttributeInt32ForAllPoints=m.prototype.GetAttributeInt32ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Cc(g,b,c,d)};m.prototype.GetAttributeUInt32ForAllPoints=m.prototype.GetAttributeUInt32ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===
|
||||
typeof d&&(d=d.ptr);return!!Dc(g,b,c,d)};m.prototype.GetAttributeDataArrayForAllPoints=m.prototype.GetAttributeDataArrayForAllPoints=function(b,c,d,g,t){var aa=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);g&&"object"===typeof g&&(g=g.ptr);t&&"object"===typeof t&&(t=t.ptr);return!!Ec(aa,b,c,d,g,t)};m.prototype.SkipAttributeTransform=m.prototype.SkipAttributeTransform=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);Fc(c,
|
||||
b)};m.prototype.GetEncodedGeometryType_Deprecated=m.prototype.GetEncodedGeometryType_Deprecated=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Gc(c,b)};m.prototype.DecodeBufferToPointCloud=m.prototype.DecodeBufferToPointCloud=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(Hc(d,b,c),C)};m.prototype.DecodeBufferToMesh=m.prototype.DecodeBufferToMesh=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===
|
||||
typeof c&&(c=c.ptr);return B(Ic(d,b,c),C)};m.prototype.__destroy__=m.prototype.__destroy__=function(){Jc(this.ptr)};(function(){function b(){a.ATTRIBUTE_INVALID_TRANSFORM=Kc();a.ATTRIBUTE_NO_TRANSFORM=Lc();a.ATTRIBUTE_QUANTIZATION_TRANSFORM=Mc();a.ATTRIBUTE_OCTAHEDRON_TRANSFORM=Nc();a.INVALID=Oc();a.POSITION=Pc();a.NORMAL=Qc();a.COLOR=Rc();a.TEX_COORD=Sc();a.GENERIC=Tc();a.INVALID_GEOMETRY_TYPE=Uc();a.POINT_CLOUD=Vc();a.TRIANGULAR_MESH=Wc();a.DT_INVALID=Xc();a.DT_INT8=Yc();a.DT_UINT8=Zc();a.DT_INT16=
|
||||
$c();a.DT_UINT16=ad();a.DT_INT32=bd();a.DT_UINT32=cd();a.DT_INT64=dd();a.DT_UINT64=ed();a.DT_FLOAT32=fd();a.DT_FLOAT64=gd();a.DT_BOOL=hd();a.DT_TYPES_COUNT=id();a.OK=jd();a.DRACO_ERROR=kd();a.IO_ERROR=ld();a.INVALID_PARAMETER=md();a.UNSUPPORTED_VERSION=nd();a.UNKNOWN_VERSION=od()}va?b():oa.unshift(b)})();if("function"===typeof a.onModuleParsed)a.onModuleParsed();a.Decoder.prototype.GetEncodedGeometryType=function(b){if(b.__class__&&b.__class__===a.DecoderBuffer)return a.Decoder.prototype.GetEncodedGeometryType_Deprecated(b);
|
||||
if(8>b.byteLength)return a.INVALID_GEOMETRY_TYPE;switch(b[7]){case 0:return a.POINT_CLOUD;case 1:return a.TRIANGULAR_MESH;default:return a.INVALID_GEOMETRY_TYPE}};return n.ready}}();"object"===typeof exports&&"object"===typeof module?module.exports=DracoDecoderModule:"function"===typeof define&&define.amd?define([],function(){return DracoDecoderModule}):"object"===typeof exports?(exports.DracoDecoderModule=DracoDecoderModule):globalThis["DracoDecoderModule"] = DracoDecoderModule;
|
||||
21
public/draco/msc_basis_transcoder.js
Normal file
BIN
public/draco/msc_basis_transcoder.wasm
Normal file
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
2
public/src/bblcdn/babylon.ktx2Decoder.js
Normal file
BIN
public/src/bblcdn/draco_decoder_gltf .wasm
Normal file
BIN
public/src/bblcdn/draco_decoder_gltf.wasm
Normal file
116
public/src/bblcdn/draco_wasm_wrapper_gltf.js
Normal file
@ -0,0 +1,116 @@
|
||||
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(h){var n=0;return function(){return n<h.length?{done:!1,value:h[n++]}:{done:!0}}};$jscomp.arrayIterator=function(h){return{next:$jscomp.arrayIteratorImpl(h)}};$jscomp.makeIterator=function(h){var n="undefined"!=typeof Symbol&&Symbol.iterator&&h[Symbol.iterator];return n?n.call(h):$jscomp.arrayIterator(h)};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;
|
||||
$jscomp.ISOLATE_POLYFILLS=!1;$jscomp.FORCE_POLYFILL_PROMISE=!1;$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION=!1;$jscomp.getGlobal=function(h){h=["object"==typeof globalThis&&globalThis,h,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var n=0;n<h.length;++n){var k=h[n];if(k&&k.Math==Math)return k}throw Error("Cannot find global object");};$jscomp.global=$jscomp.getGlobal(this);
|
||||
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(h,n,k){if(h==Array.prototype||h==Object.prototype)return h;h[n]=k.value;return h};$jscomp.IS_SYMBOL_NATIVE="function"===typeof Symbol&&"symbol"===typeof Symbol("x");$jscomp.TRUST_ES6_POLYFILLS=!$jscomp.ISOLATE_POLYFILLS||$jscomp.IS_SYMBOL_NATIVE;$jscomp.polyfills={};$jscomp.propertyToPolyfillSymbol={};$jscomp.POLYFILL_PREFIX="$jscp$";
|
||||
var $jscomp$lookupPolyfilledValue=function(h,n){var k=$jscomp.propertyToPolyfillSymbol[n];if(null==k)return h[n];k=h[k];return void 0!==k?k:h[n]};$jscomp.polyfill=function(h,n,k,p){n&&($jscomp.ISOLATE_POLYFILLS?$jscomp.polyfillIsolated(h,n,k,p):$jscomp.polyfillUnisolated(h,n,k,p))};
|
||||
$jscomp.polyfillUnisolated=function(h,n,k,p){k=$jscomp.global;h=h.split(".");for(p=0;p<h.length-1;p++){var l=h[p];if(!(l in k))return;k=k[l]}h=h[h.length-1];p=k[h];n=n(p);n!=p&&null!=n&&$jscomp.defineProperty(k,h,{configurable:!0,writable:!0,value:n})};
|
||||
$jscomp.polyfillIsolated=function(h,n,k,p){var l=h.split(".");h=1===l.length;p=l[0];p=!h&&p in $jscomp.polyfills?$jscomp.polyfills:$jscomp.global;for(var y=0;y<l.length-1;y++){var f=l[y];if(!(f in p))return;p=p[f]}l=l[l.length-1];k=$jscomp.IS_SYMBOL_NATIVE&&"es6"===k?p[l]:null;n=n(k);null!=n&&(h?$jscomp.defineProperty($jscomp.polyfills,l,{configurable:!0,writable:!0,value:n}):n!==k&&(void 0===$jscomp.propertyToPolyfillSymbol[l]&&(k=1E9*Math.random()>>>0,$jscomp.propertyToPolyfillSymbol[l]=$jscomp.IS_SYMBOL_NATIVE?
|
||||
$jscomp.global.Symbol(l):$jscomp.POLYFILL_PREFIX+k+"$"+l),$jscomp.defineProperty(p,$jscomp.propertyToPolyfillSymbol[l],{configurable:!0,writable:!0,value:n})))};
|
||||
$jscomp.polyfill("Promise",function(h){function n(){this.batch_=null}function k(f){return f instanceof l?f:new l(function(q,u){q(f)})}if(h&&(!($jscomp.FORCE_POLYFILL_PROMISE||$jscomp.FORCE_POLYFILL_PROMISE_WHEN_NO_UNHANDLED_REJECTION&&"undefined"===typeof $jscomp.global.PromiseRejectionEvent)||!$jscomp.global.Promise||-1===$jscomp.global.Promise.toString().indexOf("[native code]")))return h;n.prototype.asyncExecute=function(f){if(null==this.batch_){this.batch_=[];var q=this;this.asyncExecuteFunction(function(){q.executeBatch_()})}this.batch_.push(f)};
|
||||
var p=$jscomp.global.setTimeout;n.prototype.asyncExecuteFunction=function(f){p(f,0)};n.prototype.executeBatch_=function(){for(;this.batch_&&this.batch_.length;){var f=this.batch_;this.batch_=[];for(var q=0;q<f.length;++q){var u=f[q];f[q]=null;try{u()}catch(A){this.asyncThrow_(A)}}}this.batch_=null};n.prototype.asyncThrow_=function(f){this.asyncExecuteFunction(function(){throw f;})};var l=function(f){this.state_=0;this.result_=void 0;this.onSettledCallbacks_=[];this.isRejectionHandled_=!1;var q=this.createResolveAndReject_();
|
||||
try{f(q.resolve,q.reject)}catch(u){q.reject(u)}};l.prototype.createResolveAndReject_=function(){function f(A){return function(F){u||(u=!0,A.call(q,F))}}var q=this,u=!1;return{resolve:f(this.resolveTo_),reject:f(this.reject_)}};l.prototype.resolveTo_=function(f){if(f===this)this.reject_(new TypeError("A Promise cannot resolve to itself"));else if(f instanceof l)this.settleSameAsPromise_(f);else{a:switch(typeof f){case "object":var q=null!=f;break a;case "function":q=!0;break a;default:q=!1}q?this.resolveToNonPromiseObj_(f):
|
||||
this.fulfill_(f)}};l.prototype.resolveToNonPromiseObj_=function(f){var q=void 0;try{q=f.then}catch(u){this.reject_(u);return}"function"==typeof q?this.settleSameAsThenable_(q,f):this.fulfill_(f)};l.prototype.reject_=function(f){this.settle_(2,f)};l.prototype.fulfill_=function(f){this.settle_(1,f)};l.prototype.settle_=function(f,q){if(0!=this.state_)throw Error("Cannot settle("+f+", "+q+"): Promise already settled in state"+this.state_);this.state_=f;this.result_=q;2===this.state_&&this.scheduleUnhandledRejectionCheck_();
|
||||
this.executeOnSettledCallbacks_()};l.prototype.scheduleUnhandledRejectionCheck_=function(){var f=this;p(function(){if(f.notifyUnhandledRejection_()){var q=$jscomp.global.console;"undefined"!==typeof q&&q.error(f.result_)}},1)};l.prototype.notifyUnhandledRejection_=function(){if(this.isRejectionHandled_)return!1;var f=$jscomp.global.CustomEvent,q=$jscomp.global.Event,u=$jscomp.global.dispatchEvent;if("undefined"===typeof u)return!0;"function"===typeof f?f=new f("unhandledrejection",{cancelable:!0}):
|
||||
"function"===typeof q?f=new q("unhandledrejection",{cancelable:!0}):(f=$jscomp.global.document.createEvent("CustomEvent"),f.initCustomEvent("unhandledrejection",!1,!0,f));f.promise=this;f.reason=this.result_;return u(f)};l.prototype.executeOnSettledCallbacks_=function(){if(null!=this.onSettledCallbacks_){for(var f=0;f<this.onSettledCallbacks_.length;++f)y.asyncExecute(this.onSettledCallbacks_[f]);this.onSettledCallbacks_=null}};var y=new n;l.prototype.settleSameAsPromise_=function(f){var q=this.createResolveAndReject_();
|
||||
f.callWhenSettled_(q.resolve,q.reject)};l.prototype.settleSameAsThenable_=function(f,q){var u=this.createResolveAndReject_();try{f.call(q,u.resolve,u.reject)}catch(A){u.reject(A)}};l.prototype.then=function(f,q){function u(w,B){return"function"==typeof w?function(R){try{A(w(R))}catch(Z){F(Z)}}:B}var A,F,v=new l(function(w,B){A=w;F=B});this.callWhenSettled_(u(f,A),u(q,F));return v};l.prototype.catch=function(f){return this.then(void 0,f)};l.prototype.callWhenSettled_=function(f,q){function u(){switch(A.state_){case 1:f(A.result_);
|
||||
break;case 2:q(A.result_);break;default:throw Error("Unexpected state: "+A.state_);}}var A=this;null==this.onSettledCallbacks_?y.asyncExecute(u):this.onSettledCallbacks_.push(u);this.isRejectionHandled_=!0};l.resolve=k;l.reject=function(f){return new l(function(q,u){u(f)})};l.race=function(f){return new l(function(q,u){for(var A=$jscomp.makeIterator(f),F=A.next();!F.done;F=A.next())k(F.value).callWhenSettled_(q,u)})};l.all=function(f){var q=$jscomp.makeIterator(f),u=q.next();return u.done?k([]):new l(function(A,
|
||||
F){function v(R){return function(Z){w[R]=Z;B--;0==B&&A(w)}}var w=[],B=0;do w.push(void 0),B++,k(u.value).callWhenSettled_(v(w.length-1),F),u=q.next();while(!u.done)})};return l},"es6","es3");$jscomp.owns=function(h,n){return Object.prototype.hasOwnProperty.call(h,n)};$jscomp.assign=$jscomp.TRUST_ES6_POLYFILLS&&"function"==typeof Object.assign?Object.assign:function(h,n){for(var k=1;k<arguments.length;k++){var p=arguments[k];if(p)for(var l in p)$jscomp.owns(p,l)&&(h[l]=p[l])}return h};
|
||||
$jscomp.polyfill("Object.assign",function(h){return h||$jscomp.assign},"es6","es3");$jscomp.checkStringArgs=function(h,n,k){if(null==h)throw new TypeError("The 'this' value for String.prototype."+k+" must not be null or undefined");if(n instanceof RegExp)throw new TypeError("First argument to String.prototype."+k+" must not be a regular expression");return h+""};
|
||||
$jscomp.polyfill("String.prototype.startsWith",function(h){return h?h:function(n,k){var p=$jscomp.checkStringArgs(this,n,"startsWith");n+="";var l=p.length,y=n.length;k=Math.max(0,Math.min(k|0,p.length));for(var f=0;f<y&&k<l;)if(p[k++]!=n[f++])return!1;return f>=y}},"es6","es3");
|
||||
$jscomp.polyfill("Array.prototype.copyWithin",function(h){function n(k){k=Number(k);return Infinity===k||-Infinity===k?k:k|0}return h?h:function(k,p,l){var y=this.length;k=n(k);p=n(p);l=void 0===l?y:n(l);k=0>k?Math.max(y+k,0):Math.min(k,y);p=0>p?Math.max(y+p,0):Math.min(p,y);l=0>l?Math.max(y+l,0):Math.min(l,y);if(k<p)for(;p<l;)p in this?this[k++]=this[p++]:(delete this[k++],p++);else for(l=Math.min(l,y+p-k),k+=l-p;l>p;)--l in this?this[--k]=this[l]:delete this[--k];return this}},"es6","es3");
|
||||
$jscomp.typedArrayCopyWithin=function(h){return h?h:Array.prototype.copyWithin};$jscomp.polyfill("Int8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint8ClampedArray.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");
|
||||
$jscomp.polyfill("Uint16Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Int32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Uint32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float32Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");$jscomp.polyfill("Float64Array.prototype.copyWithin",$jscomp.typedArrayCopyWithin,"es6","es5");
|
||||
var DracoDecoderModule=function(){var h="undefined"!==typeof document&&document.currentScript?document.currentScript.src:void 0;"undefined"!==typeof __filename&&(h=h||__filename);return function(n){function k(e){return a.locateFile?a.locateFile(e,U):U+e}function p(e,b){if(e){var c=ia;var d=e+b;for(b=e;c[b]&&!(b>=d);)++b;if(16<b-e&&c.buffer&&ra)c=ra.decode(c.subarray(e,b));else{for(d="";e<b;){var g=c[e++];if(g&128){var t=c[e++]&63;if(192==(g&224))d+=String.fromCharCode((g&31)<<6|t);else{var aa=c[e++]&
|
||||
63;g=224==(g&240)?(g&15)<<12|t<<6|aa:(g&7)<<18|t<<12|aa<<6|c[e++]&63;65536>g?d+=String.fromCharCode(g):(g-=65536,d+=String.fromCharCode(55296|g>>10,56320|g&1023))}}else d+=String.fromCharCode(g)}c=d}}else c="";return c}function l(){var e=ja.buffer;a.HEAP8=W=new Int8Array(e);a.HEAP16=new Int16Array(e);a.HEAP32=ca=new Int32Array(e);a.HEAPU8=ia=new Uint8Array(e);a.HEAPU16=new Uint16Array(e);a.HEAPU32=Y=new Uint32Array(e);a.HEAPF32=new Float32Array(e);a.HEAPF64=new Float64Array(e)}function y(e){if(a.onAbort)a.onAbort(e);
|
||||
e="Aborted("+e+")";da(e);sa=!0;e=new WebAssembly.RuntimeError(e+". Build with -sASSERTIONS for more info.");ka(e);throw e;}function f(e){try{if(e==P&&ea)return new Uint8Array(ea);if(ma)return ma(e);throw"both async and sync fetching of the wasm failed";}catch(b){y(b)}}function q(){if(!ea&&(ta||fa)){if("function"==typeof fetch&&!P.startsWith("file://"))return fetch(P,{credentials:"same-origin"}).then(function(e){if(!e.ok)throw"failed to load wasm binary file at '"+P+"'";return e.arrayBuffer()}).catch(function(){return f(P)});
|
||||
if(na)return new Promise(function(e,b){na(P,function(c){e(new Uint8Array(c))},b)})}return Promise.resolve().then(function(){return f(P)})}function u(e){for(;0<e.length;)e.shift()(a)}function A(e){this.excPtr=e;this.ptr=e-24;this.set_type=function(b){Y[this.ptr+4>>2]=b};this.get_type=function(){return Y[this.ptr+4>>2]};this.set_destructor=function(b){Y[this.ptr+8>>2]=b};this.get_destructor=function(){return Y[this.ptr+8>>2]};this.set_refcount=function(b){ca[this.ptr>>2]=b};this.set_caught=function(b){W[this.ptr+
|
||||
12>>0]=b?1:0};this.get_caught=function(){return 0!=W[this.ptr+12>>0]};this.set_rethrown=function(b){W[this.ptr+13>>0]=b?1:0};this.get_rethrown=function(){return 0!=W[this.ptr+13>>0]};this.init=function(b,c){this.set_adjusted_ptr(0);this.set_type(b);this.set_destructor(c);this.set_refcount(0);this.set_caught(!1);this.set_rethrown(!1)};this.add_ref=function(){ca[this.ptr>>2]+=1};this.release_ref=function(){var b=ca[this.ptr>>2];ca[this.ptr>>2]=b-1;return 1===b};this.set_adjusted_ptr=function(b){Y[this.ptr+
|
||||
16>>2]=b};this.get_adjusted_ptr=function(){return Y[this.ptr+16>>2]};this.get_exception_ptr=function(){if(ua(this.get_type()))return Y[this.excPtr>>2];var b=this.get_adjusted_ptr();return 0!==b?b:this.excPtr}}function F(){function e(){if(!la&&(la=!0,a.calledRun=!0,!sa)){va=!0;u(oa);wa(a);if(a.onRuntimeInitialized)a.onRuntimeInitialized();if(a.postRun)for("function"==typeof a.postRun&&(a.postRun=[a.postRun]);a.postRun.length;)xa.unshift(a.postRun.shift());u(xa)}}if(!(0<ba)){if(a.preRun)for("function"==
|
||||
typeof a.preRun&&(a.preRun=[a.preRun]);a.preRun.length;)ya.unshift(a.preRun.shift());u(ya);0<ba||(a.setStatus?(a.setStatus("Running..."),setTimeout(function(){setTimeout(function(){a.setStatus("")},1);e()},1)):e())}}function v(){}function w(e){return(e||v).__cache__}function B(e,b){var c=w(b),d=c[e];if(d)return d;d=Object.create((b||v).prototype);d.ptr=e;return c[e]=d}function R(e){if("string"===typeof e){for(var b=0,c=0;c<e.length;++c){var d=e.charCodeAt(c);127>=d?b++:2047>=d?b+=2:55296<=d&&57343>=
|
||||
d?(b+=4,++c):b+=3}b=Array(b+1);c=0;d=b.length;if(0<d){d=c+d-1;for(var g=0;g<e.length;++g){var t=e.charCodeAt(g);if(55296<=t&&57343>=t){var aa=e.charCodeAt(++g);t=65536+((t&1023)<<10)|aa&1023}if(127>=t){if(c>=d)break;b[c++]=t}else{if(2047>=t){if(c+1>=d)break;b[c++]=192|t>>6}else{if(65535>=t){if(c+2>=d)break;b[c++]=224|t>>12}else{if(c+3>=d)break;b[c++]=240|t>>18;b[c++]=128|t>>12&63}b[c++]=128|t>>6&63}b[c++]=128|t&63}}b[c]=0}e=r.alloc(b,W);r.copy(b,W,e);return e}return e}function Z(e){if("object"===
|
||||
typeof e){var b=r.alloc(e,W);r.copy(e,W,b);return b}return e}function X(){throw"cannot construct a VoidPtr, no constructor in IDL";}function S(){this.ptr=za();w(S)[this.ptr]=this}function Q(){this.ptr=Aa();w(Q)[this.ptr]=this}function V(){this.ptr=Ba();w(V)[this.ptr]=this}function x(){this.ptr=Ca();w(x)[this.ptr]=this}function D(){this.ptr=Da();w(D)[this.ptr]=this}function G(){this.ptr=Ea();w(G)[this.ptr]=this}function H(){this.ptr=Fa();w(H)[this.ptr]=this}function E(){this.ptr=Ga();w(E)[this.ptr]=
|
||||
this}function T(){this.ptr=Ha();w(T)[this.ptr]=this}function C(){throw"cannot construct a Status, no constructor in IDL";}function I(){this.ptr=Ia();w(I)[this.ptr]=this}function J(){this.ptr=Ja();w(J)[this.ptr]=this}function K(){this.ptr=Ka();w(K)[this.ptr]=this}function L(){this.ptr=La();w(L)[this.ptr]=this}function M(){this.ptr=Ma();w(M)[this.ptr]=this}function N(){this.ptr=Na();w(N)[this.ptr]=this}function O(){this.ptr=Oa();w(O)[this.ptr]=this}function z(){this.ptr=Pa();w(z)[this.ptr]=this}function m(){this.ptr=
|
||||
Qa();w(m)[this.ptr]=this}n=void 0===n?{}:n;var a="undefined"!=typeof n?n:{},wa,ka;a.ready=new Promise(function(e,b){wa=e;ka=b});var Ra=!1,Sa=!1;a.onRuntimeInitialized=function(){Ra=!0;if(Sa&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.onModuleParsed=function(){Sa=!0;if(Ra&&"function"===typeof a.onModuleLoaded)a.onModuleLoaded(a)};a.isVersionSupported=function(e){if("string"!==typeof e)return!1;e=e.split(".");return 2>e.length||3<e.length?!1:1==e[0]&&0<=e[1]&&5>=e[1]?!0:0!=e[0]||10<
|
||||
e[1]?!1:!0};var Ta=Object.assign({},a),ta="object"==typeof window,fa="function"==typeof importScripts,Ua="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,U="";if(Ua){var Va=require("fs"),pa=require("path");U=fa?pa.dirname(U)+"/":__dirname+"/";var Wa=function(e,b){e=e.startsWith("file://")?new URL(e):pa.normalize(e);return Va.readFileSync(e,b?void 0:"utf8")};var ma=function(e){e=Wa(e,!0);e.buffer||(e=new Uint8Array(e));return e};var na=function(e,
|
||||
b,c){e=e.startsWith("file://")?new URL(e):pa.normalize(e);Va.readFile(e,function(d,g){d?c(d):b(g.buffer)})};1<process.argv.length&&process.argv[1].replace(/\\/g,"/");process.argv.slice(2);a.inspect=function(){return"[Emscripten Module object]"}}else if(ta||fa)fa?U=self.location.href:"undefined"!=typeof document&&document.currentScript&&(U=document.currentScript.src),h&&(U=h),U=0!==U.indexOf("blob:")?U.substr(0,U.replace(/[?#].*/,"").lastIndexOf("/")+1):"",Wa=function(e){var b=new XMLHttpRequest;b.open("GET",
|
||||
e,!1);b.send(null);return b.responseText},fa&&(ma=function(e){var b=new XMLHttpRequest;b.open("GET",e,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)}),na=function(e,b,c){var d=new XMLHttpRequest;d.open("GET",e,!0);d.responseType="arraybuffer";d.onload=function(){200==d.status||0==d.status&&d.response?b(d.response):c()};d.onerror=c;d.send(null)};a.print||console.log.bind(console);var da=a.printErr||console.warn.bind(console);Object.assign(a,Ta);Ta=null;var ea;a.wasmBinary&&
|
||||
(ea=a.wasmBinary);"object"!=typeof WebAssembly&&y("no native wasm support detected");var ja,sa=!1,ra="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0,W,ia,ca,Y,ya=[],oa=[],xa=[],va=!1,ba=0,qa=null,ha=null;var P="draco_decoder_gltf.wasm";P.startsWith("data:application/octet-stream;base64,")||(P=k(P));var pd=0,qd={b:function(e,b,c){(new A(e)).init(b,c);pd++;throw e;},a:function(){y("")},d:function(e,b,c){ia.copyWithin(e,b,b+c)},c:function(e){var b=ia.length;e>>>=0;if(2147483648<e)return!1;
|
||||
for(var c=1;4>=c;c*=2){var d=b*(1+.2/c);d=Math.min(d,e+100663296);var g=Math;d=Math.max(e,d);g=g.min.call(g,2147483648,d+(65536-d%65536)%65536);a:{d=ja.buffer;try{ja.grow(g-d.byteLength+65535>>>16);l();var t=1;break a}catch(aa){}t=void 0}if(t)return!0}return!1}};(function(){function e(g,t){a.asm=g.exports;ja=a.asm.e;l();oa.unshift(a.asm.f);ba--;a.monitorRunDependencies&&a.monitorRunDependencies(ba);0==ba&&(null!==qa&&(clearInterval(qa),qa=null),ha&&(g=ha,ha=null,g()))}function b(g){e(g.instance)}
|
||||
function c(g){return q().then(function(t){return WebAssembly.instantiate(t,d)}).then(function(t){return t}).then(g,function(t){da("failed to asynchronously prepare wasm: "+t);y(t)})}var d={a:qd};ba++;a.monitorRunDependencies&&a.monitorRunDependencies(ba);if(a.instantiateWasm)try{return a.instantiateWasm(d,e)}catch(g){da("Module.instantiateWasm callback failed with error: "+g),ka(g)}(function(){return ea||"function"!=typeof WebAssembly.instantiateStreaming||P.startsWith("data:application/octet-stream;base64,")||
|
||||
P.startsWith("file://")||Ua||"function"!=typeof fetch?c(b):fetch(P,{credentials:"same-origin"}).then(function(g){return WebAssembly.instantiateStreaming(g,d).then(b,function(t){da("wasm streaming compile failed: "+t);da("falling back to ArrayBuffer instantiation");return c(b)})})})().catch(ka);return{}})();var Xa=a._emscripten_bind_VoidPtr___destroy___0=function(){return(Xa=a._emscripten_bind_VoidPtr___destroy___0=a.asm.h).apply(null,arguments)},za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=
|
||||
function(){return(za=a._emscripten_bind_DecoderBuffer_DecoderBuffer_0=a.asm.i).apply(null,arguments)},Ya=a._emscripten_bind_DecoderBuffer_Init_2=function(){return(Ya=a._emscripten_bind_DecoderBuffer_Init_2=a.asm.j).apply(null,arguments)},Za=a._emscripten_bind_DecoderBuffer___destroy___0=function(){return(Za=a._emscripten_bind_DecoderBuffer___destroy___0=a.asm.k).apply(null,arguments)},Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=function(){return(Aa=a._emscripten_bind_AttributeTransformData_AttributeTransformData_0=
|
||||
a.asm.l).apply(null,arguments)},$a=a._emscripten_bind_AttributeTransformData_transform_type_0=function(){return($a=a._emscripten_bind_AttributeTransformData_transform_type_0=a.asm.m).apply(null,arguments)},ab=a._emscripten_bind_AttributeTransformData___destroy___0=function(){return(ab=a._emscripten_bind_AttributeTransformData___destroy___0=a.asm.n).apply(null,arguments)},Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=function(){return(Ba=a._emscripten_bind_GeometryAttribute_GeometryAttribute_0=
|
||||
a.asm.o).apply(null,arguments)},bb=a._emscripten_bind_GeometryAttribute___destroy___0=function(){return(bb=a._emscripten_bind_GeometryAttribute___destroy___0=a.asm.p).apply(null,arguments)},Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=function(){return(Ca=a._emscripten_bind_PointAttribute_PointAttribute_0=a.asm.q).apply(null,arguments)},cb=a._emscripten_bind_PointAttribute_size_0=function(){return(cb=a._emscripten_bind_PointAttribute_size_0=a.asm.r).apply(null,arguments)},db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=
|
||||
function(){return(db=a._emscripten_bind_PointAttribute_GetAttributeTransformData_0=a.asm.s).apply(null,arguments)},eb=a._emscripten_bind_PointAttribute_attribute_type_0=function(){return(eb=a._emscripten_bind_PointAttribute_attribute_type_0=a.asm.t).apply(null,arguments)},fb=a._emscripten_bind_PointAttribute_data_type_0=function(){return(fb=a._emscripten_bind_PointAttribute_data_type_0=a.asm.u).apply(null,arguments)},gb=a._emscripten_bind_PointAttribute_num_components_0=function(){return(gb=a._emscripten_bind_PointAttribute_num_components_0=
|
||||
a.asm.v).apply(null,arguments)},hb=a._emscripten_bind_PointAttribute_normalized_0=function(){return(hb=a._emscripten_bind_PointAttribute_normalized_0=a.asm.w).apply(null,arguments)},ib=a._emscripten_bind_PointAttribute_byte_stride_0=function(){return(ib=a._emscripten_bind_PointAttribute_byte_stride_0=a.asm.x).apply(null,arguments)},jb=a._emscripten_bind_PointAttribute_byte_offset_0=function(){return(jb=a._emscripten_bind_PointAttribute_byte_offset_0=a.asm.y).apply(null,arguments)},kb=a._emscripten_bind_PointAttribute_unique_id_0=
|
||||
function(){return(kb=a._emscripten_bind_PointAttribute_unique_id_0=a.asm.z).apply(null,arguments)},lb=a._emscripten_bind_PointAttribute___destroy___0=function(){return(lb=a._emscripten_bind_PointAttribute___destroy___0=a.asm.A).apply(null,arguments)},Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=function(){return(Da=a._emscripten_bind_AttributeQuantizationTransform_AttributeQuantizationTransform_0=a.asm.B).apply(null,arguments)},mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=
|
||||
function(){return(mb=a._emscripten_bind_AttributeQuantizationTransform_InitFromAttribute_1=a.asm.C).apply(null,arguments)},nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=function(){return(nb=a._emscripten_bind_AttributeQuantizationTransform_quantization_bits_0=a.asm.D).apply(null,arguments)},ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=function(){return(ob=a._emscripten_bind_AttributeQuantizationTransform_min_value_1=a.asm.E).apply(null,arguments)},pb=
|
||||
a._emscripten_bind_AttributeQuantizationTransform_range_0=function(){return(pb=a._emscripten_bind_AttributeQuantizationTransform_range_0=a.asm.F).apply(null,arguments)},qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=function(){return(qb=a._emscripten_bind_AttributeQuantizationTransform___destroy___0=a.asm.G).apply(null,arguments)},Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=function(){return(Ea=a._emscripten_bind_AttributeOctahedronTransform_AttributeOctahedronTransform_0=
|
||||
a.asm.H).apply(null,arguments)},rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=function(){return(rb=a._emscripten_bind_AttributeOctahedronTransform_InitFromAttribute_1=a.asm.I).apply(null,arguments)},sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=function(){return(sb=a._emscripten_bind_AttributeOctahedronTransform_quantization_bits_0=a.asm.J).apply(null,arguments)},tb=a._emscripten_bind_AttributeOctahedronTransform___destroy___0=function(){return(tb=
|
||||
a._emscripten_bind_AttributeOctahedronTransform___destroy___0=a.asm.K).apply(null,arguments)},Fa=a._emscripten_bind_PointCloud_PointCloud_0=function(){return(Fa=a._emscripten_bind_PointCloud_PointCloud_0=a.asm.L).apply(null,arguments)},ub=a._emscripten_bind_PointCloud_num_attributes_0=function(){return(ub=a._emscripten_bind_PointCloud_num_attributes_0=a.asm.M).apply(null,arguments)},vb=a._emscripten_bind_PointCloud_num_points_0=function(){return(vb=a._emscripten_bind_PointCloud_num_points_0=a.asm.N).apply(null,
|
||||
arguments)},wb=a._emscripten_bind_PointCloud___destroy___0=function(){return(wb=a._emscripten_bind_PointCloud___destroy___0=a.asm.O).apply(null,arguments)},Ga=a._emscripten_bind_Mesh_Mesh_0=function(){return(Ga=a._emscripten_bind_Mesh_Mesh_0=a.asm.P).apply(null,arguments)},xb=a._emscripten_bind_Mesh_num_faces_0=function(){return(xb=a._emscripten_bind_Mesh_num_faces_0=a.asm.Q).apply(null,arguments)},yb=a._emscripten_bind_Mesh_num_attributes_0=function(){return(yb=a._emscripten_bind_Mesh_num_attributes_0=
|
||||
a.asm.R).apply(null,arguments)},zb=a._emscripten_bind_Mesh_num_points_0=function(){return(zb=a._emscripten_bind_Mesh_num_points_0=a.asm.S).apply(null,arguments)},Ab=a._emscripten_bind_Mesh___destroy___0=function(){return(Ab=a._emscripten_bind_Mesh___destroy___0=a.asm.T).apply(null,arguments)},Ha=a._emscripten_bind_Metadata_Metadata_0=function(){return(Ha=a._emscripten_bind_Metadata_Metadata_0=a.asm.U).apply(null,arguments)},Bb=a._emscripten_bind_Metadata___destroy___0=function(){return(Bb=a._emscripten_bind_Metadata___destroy___0=
|
||||
a.asm.V).apply(null,arguments)},Cb=a._emscripten_bind_Status_code_0=function(){return(Cb=a._emscripten_bind_Status_code_0=a.asm.W).apply(null,arguments)},Db=a._emscripten_bind_Status_ok_0=function(){return(Db=a._emscripten_bind_Status_ok_0=a.asm.X).apply(null,arguments)},Eb=a._emscripten_bind_Status_error_msg_0=function(){return(Eb=a._emscripten_bind_Status_error_msg_0=a.asm.Y).apply(null,arguments)},Fb=a._emscripten_bind_Status___destroy___0=function(){return(Fb=a._emscripten_bind_Status___destroy___0=
|
||||
a.asm.Z).apply(null,arguments)},Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=function(){return(Ia=a._emscripten_bind_DracoFloat32Array_DracoFloat32Array_0=a.asm._).apply(null,arguments)},Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=function(){return(Gb=a._emscripten_bind_DracoFloat32Array_GetValue_1=a.asm.$).apply(null,arguments)},Hb=a._emscripten_bind_DracoFloat32Array_size_0=function(){return(Hb=a._emscripten_bind_DracoFloat32Array_size_0=a.asm.aa).apply(null,arguments)},Ib=
|
||||
a._emscripten_bind_DracoFloat32Array___destroy___0=function(){return(Ib=a._emscripten_bind_DracoFloat32Array___destroy___0=a.asm.ba).apply(null,arguments)},Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=function(){return(Ja=a._emscripten_bind_DracoInt8Array_DracoInt8Array_0=a.asm.ca).apply(null,arguments)},Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=function(){return(Jb=a._emscripten_bind_DracoInt8Array_GetValue_1=a.asm.da).apply(null,arguments)},Kb=a._emscripten_bind_DracoInt8Array_size_0=
|
||||
function(){return(Kb=a._emscripten_bind_DracoInt8Array_size_0=a.asm.ea).apply(null,arguments)},Lb=a._emscripten_bind_DracoInt8Array___destroy___0=function(){return(Lb=a._emscripten_bind_DracoInt8Array___destroy___0=a.asm.fa).apply(null,arguments)},Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=function(){return(Ka=a._emscripten_bind_DracoUInt8Array_DracoUInt8Array_0=a.asm.ga).apply(null,arguments)},Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=function(){return(Mb=a._emscripten_bind_DracoUInt8Array_GetValue_1=
|
||||
a.asm.ha).apply(null,arguments)},Nb=a._emscripten_bind_DracoUInt8Array_size_0=function(){return(Nb=a._emscripten_bind_DracoUInt8Array_size_0=a.asm.ia).apply(null,arguments)},Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=function(){return(Ob=a._emscripten_bind_DracoUInt8Array___destroy___0=a.asm.ja).apply(null,arguments)},La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=function(){return(La=a._emscripten_bind_DracoInt16Array_DracoInt16Array_0=a.asm.ka).apply(null,arguments)},Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=
|
||||
function(){return(Pb=a._emscripten_bind_DracoInt16Array_GetValue_1=a.asm.la).apply(null,arguments)},Qb=a._emscripten_bind_DracoInt16Array_size_0=function(){return(Qb=a._emscripten_bind_DracoInt16Array_size_0=a.asm.ma).apply(null,arguments)},Rb=a._emscripten_bind_DracoInt16Array___destroy___0=function(){return(Rb=a._emscripten_bind_DracoInt16Array___destroy___0=a.asm.na).apply(null,arguments)},Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=function(){return(Ma=a._emscripten_bind_DracoUInt16Array_DracoUInt16Array_0=
|
||||
a.asm.oa).apply(null,arguments)},Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=function(){return(Sb=a._emscripten_bind_DracoUInt16Array_GetValue_1=a.asm.pa).apply(null,arguments)},Tb=a._emscripten_bind_DracoUInt16Array_size_0=function(){return(Tb=a._emscripten_bind_DracoUInt16Array_size_0=a.asm.qa).apply(null,arguments)},Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=function(){return(Ub=a._emscripten_bind_DracoUInt16Array___destroy___0=a.asm.ra).apply(null,arguments)},Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=
|
||||
function(){return(Na=a._emscripten_bind_DracoInt32Array_DracoInt32Array_0=a.asm.sa).apply(null,arguments)},Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=function(){return(Vb=a._emscripten_bind_DracoInt32Array_GetValue_1=a.asm.ta).apply(null,arguments)},Wb=a._emscripten_bind_DracoInt32Array_size_0=function(){return(Wb=a._emscripten_bind_DracoInt32Array_size_0=a.asm.ua).apply(null,arguments)},Xb=a._emscripten_bind_DracoInt32Array___destroy___0=function(){return(Xb=a._emscripten_bind_DracoInt32Array___destroy___0=
|
||||
a.asm.va).apply(null,arguments)},Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=function(){return(Oa=a._emscripten_bind_DracoUInt32Array_DracoUInt32Array_0=a.asm.wa).apply(null,arguments)},Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=function(){return(Yb=a._emscripten_bind_DracoUInt32Array_GetValue_1=a.asm.xa).apply(null,arguments)},Zb=a._emscripten_bind_DracoUInt32Array_size_0=function(){return(Zb=a._emscripten_bind_DracoUInt32Array_size_0=a.asm.ya).apply(null,arguments)},$b=a._emscripten_bind_DracoUInt32Array___destroy___0=
|
||||
function(){return($b=a._emscripten_bind_DracoUInt32Array___destroy___0=a.asm.za).apply(null,arguments)},Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=function(){return(Pa=a._emscripten_bind_MetadataQuerier_MetadataQuerier_0=a.asm.Aa).apply(null,arguments)},ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=function(){return(ac=a._emscripten_bind_MetadataQuerier_HasEntry_2=a.asm.Ba).apply(null,arguments)},bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=function(){return(bc=a._emscripten_bind_MetadataQuerier_GetIntEntry_2=
|
||||
a.asm.Ca).apply(null,arguments)},cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=function(){return(cc=a._emscripten_bind_MetadataQuerier_GetIntEntryArray_3=a.asm.Da).apply(null,arguments)},dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=function(){return(dc=a._emscripten_bind_MetadataQuerier_GetDoubleEntry_2=a.asm.Ea).apply(null,arguments)},ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=function(){return(ec=a._emscripten_bind_MetadataQuerier_GetStringEntry_2=a.asm.Fa).apply(null,
|
||||
arguments)},fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=function(){return(fc=a._emscripten_bind_MetadataQuerier_NumEntries_1=a.asm.Ga).apply(null,arguments)},gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=function(){return(gc=a._emscripten_bind_MetadataQuerier_GetEntryName_2=a.asm.Ha).apply(null,arguments)},hc=a._emscripten_bind_MetadataQuerier___destroy___0=function(){return(hc=a._emscripten_bind_MetadataQuerier___destroy___0=a.asm.Ia).apply(null,arguments)},Qa=a._emscripten_bind_Decoder_Decoder_0=
|
||||
function(){return(Qa=a._emscripten_bind_Decoder_Decoder_0=a.asm.Ja).apply(null,arguments)},ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=function(){return(ic=a._emscripten_bind_Decoder_DecodeArrayToPointCloud_3=a.asm.Ka).apply(null,arguments)},jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=function(){return(jc=a._emscripten_bind_Decoder_DecodeArrayToMesh_3=a.asm.La).apply(null,arguments)},kc=a._emscripten_bind_Decoder_GetAttributeId_2=function(){return(kc=a._emscripten_bind_Decoder_GetAttributeId_2=
|
||||
a.asm.Ma).apply(null,arguments)},lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=function(){return(lc=a._emscripten_bind_Decoder_GetAttributeIdByName_2=a.asm.Na).apply(null,arguments)},mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=function(){return(mc=a._emscripten_bind_Decoder_GetAttributeIdByMetadataEntry_3=a.asm.Oa).apply(null,arguments)},nc=a._emscripten_bind_Decoder_GetAttribute_2=function(){return(nc=a._emscripten_bind_Decoder_GetAttribute_2=a.asm.Pa).apply(null,arguments)},
|
||||
oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=function(){return(oc=a._emscripten_bind_Decoder_GetAttributeByUniqueId_2=a.asm.Qa).apply(null,arguments)},pc=a._emscripten_bind_Decoder_GetMetadata_1=function(){return(pc=a._emscripten_bind_Decoder_GetMetadata_1=a.asm.Ra).apply(null,arguments)},qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=function(){return(qc=a._emscripten_bind_Decoder_GetAttributeMetadata_2=a.asm.Sa).apply(null,arguments)},rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=
|
||||
function(){return(rc=a._emscripten_bind_Decoder_GetFaceFromMesh_3=a.asm.Ta).apply(null,arguments)},sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=function(){return(sc=a._emscripten_bind_Decoder_GetTriangleStripsFromMesh_2=a.asm.Ua).apply(null,arguments)},tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=function(){return(tc=a._emscripten_bind_Decoder_GetTrianglesUInt16Array_3=a.asm.Va).apply(null,arguments)},uc=a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=function(){return(uc=
|
||||
a._emscripten_bind_Decoder_GetTrianglesUInt32Array_3=a.asm.Wa).apply(null,arguments)},vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=function(){return(vc=a._emscripten_bind_Decoder_GetAttributeFloat_3=a.asm.Xa).apply(null,arguments)},wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=function(){return(wc=a._emscripten_bind_Decoder_GetAttributeFloatForAllPoints_3=a.asm.Ya).apply(null,arguments)},xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=function(){return(xc=a._emscripten_bind_Decoder_GetAttributeIntForAllPoints_3=
|
||||
a.asm.Za).apply(null,arguments)},yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=function(){return(yc=a._emscripten_bind_Decoder_GetAttributeInt8ForAllPoints_3=a.asm._a).apply(null,arguments)},zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=function(){return(zc=a._emscripten_bind_Decoder_GetAttributeUInt8ForAllPoints_3=a.asm.$a).apply(null,arguments)},Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=function(){return(Ac=a._emscripten_bind_Decoder_GetAttributeInt16ForAllPoints_3=
|
||||
a.asm.ab).apply(null,arguments)},Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=function(){return(Bc=a._emscripten_bind_Decoder_GetAttributeUInt16ForAllPoints_3=a.asm.bb).apply(null,arguments)},Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=function(){return(Cc=a._emscripten_bind_Decoder_GetAttributeInt32ForAllPoints_3=a.asm.cb).apply(null,arguments)},Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=function(){return(Dc=a._emscripten_bind_Decoder_GetAttributeUInt32ForAllPoints_3=
|
||||
a.asm.db).apply(null,arguments)},Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=function(){return(Ec=a._emscripten_bind_Decoder_GetAttributeDataArrayForAllPoints_5=a.asm.eb).apply(null,arguments)},Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=function(){return(Fc=a._emscripten_bind_Decoder_SkipAttributeTransform_1=a.asm.fb).apply(null,arguments)},Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=function(){return(Gc=a._emscripten_bind_Decoder_GetEncodedGeometryType_Deprecated_1=
|
||||
a.asm.gb).apply(null,arguments)},Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=function(){return(Hc=a._emscripten_bind_Decoder_DecodeBufferToPointCloud_2=a.asm.hb).apply(null,arguments)},Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=function(){return(Ic=a._emscripten_bind_Decoder_DecodeBufferToMesh_2=a.asm.ib).apply(null,arguments)},Jc=a._emscripten_bind_Decoder___destroy___0=function(){return(Jc=a._emscripten_bind_Decoder___destroy___0=a.asm.jb).apply(null,arguments)},Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=
|
||||
function(){return(Kc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_INVALID_TRANSFORM=a.asm.kb).apply(null,arguments)},Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=function(){return(Lc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_NO_TRANSFORM=a.asm.lb).apply(null,arguments)},Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=function(){return(Mc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_QUANTIZATION_TRANSFORM=
|
||||
a.asm.mb).apply(null,arguments)},Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=function(){return(Nc=a._emscripten_enum_draco_AttributeTransformType_ATTRIBUTE_OCTAHEDRON_TRANSFORM=a.asm.nb).apply(null,arguments)},Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=function(){return(Oc=a._emscripten_enum_draco_GeometryAttribute_Type_INVALID=a.asm.ob).apply(null,arguments)},Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=function(){return(Pc=a._emscripten_enum_draco_GeometryAttribute_Type_POSITION=
|
||||
a.asm.pb).apply(null,arguments)},Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=function(){return(Qc=a._emscripten_enum_draco_GeometryAttribute_Type_NORMAL=a.asm.qb).apply(null,arguments)},Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=function(){return(Rc=a._emscripten_enum_draco_GeometryAttribute_Type_COLOR=a.asm.rb).apply(null,arguments)},Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=function(){return(Sc=a._emscripten_enum_draco_GeometryAttribute_Type_TEX_COORD=
|
||||
a.asm.sb).apply(null,arguments)},Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=function(){return(Tc=a._emscripten_enum_draco_GeometryAttribute_Type_GENERIC=a.asm.tb).apply(null,arguments)},Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=function(){return(Uc=a._emscripten_enum_draco_EncodedGeometryType_INVALID_GEOMETRY_TYPE=a.asm.ub).apply(null,arguments)},Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=function(){return(Vc=a._emscripten_enum_draco_EncodedGeometryType_POINT_CLOUD=
|
||||
a.asm.vb).apply(null,arguments)},Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=function(){return(Wc=a._emscripten_enum_draco_EncodedGeometryType_TRIANGULAR_MESH=a.asm.wb).apply(null,arguments)},Xc=a._emscripten_enum_draco_DataType_DT_INVALID=function(){return(Xc=a._emscripten_enum_draco_DataType_DT_INVALID=a.asm.xb).apply(null,arguments)},Yc=a._emscripten_enum_draco_DataType_DT_INT8=function(){return(Yc=a._emscripten_enum_draco_DataType_DT_INT8=a.asm.yb).apply(null,arguments)},Zc=
|
||||
a._emscripten_enum_draco_DataType_DT_UINT8=function(){return(Zc=a._emscripten_enum_draco_DataType_DT_UINT8=a.asm.zb).apply(null,arguments)},$c=a._emscripten_enum_draco_DataType_DT_INT16=function(){return($c=a._emscripten_enum_draco_DataType_DT_INT16=a.asm.Ab).apply(null,arguments)},ad=a._emscripten_enum_draco_DataType_DT_UINT16=function(){return(ad=a._emscripten_enum_draco_DataType_DT_UINT16=a.asm.Bb).apply(null,arguments)},bd=a._emscripten_enum_draco_DataType_DT_INT32=function(){return(bd=a._emscripten_enum_draco_DataType_DT_INT32=
|
||||
a.asm.Cb).apply(null,arguments)},cd=a._emscripten_enum_draco_DataType_DT_UINT32=function(){return(cd=a._emscripten_enum_draco_DataType_DT_UINT32=a.asm.Db).apply(null,arguments)},dd=a._emscripten_enum_draco_DataType_DT_INT64=function(){return(dd=a._emscripten_enum_draco_DataType_DT_INT64=a.asm.Eb).apply(null,arguments)},ed=a._emscripten_enum_draco_DataType_DT_UINT64=function(){return(ed=a._emscripten_enum_draco_DataType_DT_UINT64=a.asm.Fb).apply(null,arguments)},fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=
|
||||
function(){return(fd=a._emscripten_enum_draco_DataType_DT_FLOAT32=a.asm.Gb).apply(null,arguments)},gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=function(){return(gd=a._emscripten_enum_draco_DataType_DT_FLOAT64=a.asm.Hb).apply(null,arguments)},hd=a._emscripten_enum_draco_DataType_DT_BOOL=function(){return(hd=a._emscripten_enum_draco_DataType_DT_BOOL=a.asm.Ib).apply(null,arguments)},id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=function(){return(id=a._emscripten_enum_draco_DataType_DT_TYPES_COUNT=
|
||||
a.asm.Jb).apply(null,arguments)},jd=a._emscripten_enum_draco_StatusCode_OK=function(){return(jd=a._emscripten_enum_draco_StatusCode_OK=a.asm.Kb).apply(null,arguments)},kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=function(){return(kd=a._emscripten_enum_draco_StatusCode_DRACO_ERROR=a.asm.Lb).apply(null,arguments)},ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=function(){return(ld=a._emscripten_enum_draco_StatusCode_IO_ERROR=a.asm.Mb).apply(null,arguments)},md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=
|
||||
function(){return(md=a._emscripten_enum_draco_StatusCode_INVALID_PARAMETER=a.asm.Nb).apply(null,arguments)},nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=function(){return(nd=a._emscripten_enum_draco_StatusCode_UNSUPPORTED_VERSION=a.asm.Ob).apply(null,arguments)},od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=function(){return(od=a._emscripten_enum_draco_StatusCode_UNKNOWN_VERSION=a.asm.Pb).apply(null,arguments)};a._malloc=function(){return(a._malloc=a.asm.Qb).apply(null,arguments)};
|
||||
a._free=function(){return(a._free=a.asm.Rb).apply(null,arguments)};var ua=function(){return(ua=a.asm.Sb).apply(null,arguments)};a.___start_em_js=11660;a.___stop_em_js=11758;var la;ha=function b(){la||F();la||(ha=b)};if(a.preInit)for("function"==typeof a.preInit&&(a.preInit=[a.preInit]);0<a.preInit.length;)a.preInit.pop()();F();v.prototype=Object.create(v.prototype);v.prototype.constructor=v;v.prototype.__class__=v;v.__cache__={};a.WrapperObject=v;a.getCache=w;a.wrapPointer=B;a.castObject=function(b,
|
||||
c){return B(b.ptr,c)};a.NULL=B(0);a.destroy=function(b){if(!b.__destroy__)throw"Error: Cannot destroy object. (Did you create it yourself?)";b.__destroy__();delete w(b.__class__)[b.ptr]};a.compare=function(b,c){return b.ptr===c.ptr};a.getPointer=function(b){return b.ptr};a.getClass=function(b){return b.__class__};var r={buffer:0,size:0,pos:0,temps:[],needed:0,prepare:function(){if(r.needed){for(var b=0;b<r.temps.length;b++)a._free(r.temps[b]);r.temps.length=0;a._free(r.buffer);r.buffer=0;r.size+=
|
||||
r.needed;r.needed=0}r.buffer||(r.size+=128,r.buffer=a._malloc(r.size),r.buffer||y(void 0));r.pos=0},alloc:function(b,c){r.buffer||y(void 0);b=b.length*c.BYTES_PER_ELEMENT;b=b+7&-8;r.pos+b>=r.size?(0<b||y(void 0),r.needed+=b,c=a._malloc(b),r.temps.push(c)):(c=r.buffer+r.pos,r.pos+=b);return c},copy:function(b,c,d){d>>>=0;switch(c.BYTES_PER_ELEMENT){case 2:d>>>=1;break;case 4:d>>>=2;break;case 8:d>>>=3}for(var g=0;g<b.length;g++)c[d+g]=b[g]}};X.prototype=Object.create(v.prototype);X.prototype.constructor=
|
||||
X;X.prototype.__class__=X;X.__cache__={};a.VoidPtr=X;X.prototype.__destroy__=X.prototype.__destroy__=function(){Xa(this.ptr)};S.prototype=Object.create(v.prototype);S.prototype.constructor=S;S.prototype.__class__=S;S.__cache__={};a.DecoderBuffer=S;S.prototype.Init=S.prototype.Init=function(b,c){var d=this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);Ya(d,b,c)};S.prototype.__destroy__=S.prototype.__destroy__=function(){Za(this.ptr)};Q.prototype=Object.create(v.prototype);
|
||||
Q.prototype.constructor=Q;Q.prototype.__class__=Q;Q.__cache__={};a.AttributeTransformData=Q;Q.prototype.transform_type=Q.prototype.transform_type=function(){return $a(this.ptr)};Q.prototype.__destroy__=Q.prototype.__destroy__=function(){ab(this.ptr)};V.prototype=Object.create(v.prototype);V.prototype.constructor=V;V.prototype.__class__=V;V.__cache__={};a.GeometryAttribute=V;V.prototype.__destroy__=V.prototype.__destroy__=function(){bb(this.ptr)};x.prototype=Object.create(v.prototype);x.prototype.constructor=
|
||||
x;x.prototype.__class__=x;x.__cache__={};a.PointAttribute=x;x.prototype.size=x.prototype.size=function(){return cb(this.ptr)};x.prototype.GetAttributeTransformData=x.prototype.GetAttributeTransformData=function(){return B(db(this.ptr),Q)};x.prototype.attribute_type=x.prototype.attribute_type=function(){return eb(this.ptr)};x.prototype.data_type=x.prototype.data_type=function(){return fb(this.ptr)};x.prototype.num_components=x.prototype.num_components=function(){return gb(this.ptr)};x.prototype.normalized=
|
||||
x.prototype.normalized=function(){return!!hb(this.ptr)};x.prototype.byte_stride=x.prototype.byte_stride=function(){return ib(this.ptr)};x.prototype.byte_offset=x.prototype.byte_offset=function(){return jb(this.ptr)};x.prototype.unique_id=x.prototype.unique_id=function(){return kb(this.ptr)};x.prototype.__destroy__=x.prototype.__destroy__=function(){lb(this.ptr)};D.prototype=Object.create(v.prototype);D.prototype.constructor=D;D.prototype.__class__=D;D.__cache__={};a.AttributeQuantizationTransform=
|
||||
D;D.prototype.InitFromAttribute=D.prototype.InitFromAttribute=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return!!mb(c,b)};D.prototype.quantization_bits=D.prototype.quantization_bits=function(){return nb(this.ptr)};D.prototype.min_value=D.prototype.min_value=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return ob(c,b)};D.prototype.range=D.prototype.range=function(){return pb(this.ptr)};D.prototype.__destroy__=D.prototype.__destroy__=function(){qb(this.ptr)};G.prototype=
|
||||
Object.create(v.prototype);G.prototype.constructor=G;G.prototype.__class__=G;G.__cache__={};a.AttributeOctahedronTransform=G;G.prototype.InitFromAttribute=G.prototype.InitFromAttribute=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return!!rb(c,b)};G.prototype.quantization_bits=G.prototype.quantization_bits=function(){return sb(this.ptr)};G.prototype.__destroy__=G.prototype.__destroy__=function(){tb(this.ptr)};H.prototype=Object.create(v.prototype);H.prototype.constructor=H;H.prototype.__class__=
|
||||
H;H.__cache__={};a.PointCloud=H;H.prototype.num_attributes=H.prototype.num_attributes=function(){return ub(this.ptr)};H.prototype.num_points=H.prototype.num_points=function(){return vb(this.ptr)};H.prototype.__destroy__=H.prototype.__destroy__=function(){wb(this.ptr)};E.prototype=Object.create(v.prototype);E.prototype.constructor=E;E.prototype.__class__=E;E.__cache__={};a.Mesh=E;E.prototype.num_faces=E.prototype.num_faces=function(){return xb(this.ptr)};E.prototype.num_attributes=E.prototype.num_attributes=
|
||||
function(){return yb(this.ptr)};E.prototype.num_points=E.prototype.num_points=function(){return zb(this.ptr)};E.prototype.__destroy__=E.prototype.__destroy__=function(){Ab(this.ptr)};T.prototype=Object.create(v.prototype);T.prototype.constructor=T;T.prototype.__class__=T;T.__cache__={};a.Metadata=T;T.prototype.__destroy__=T.prototype.__destroy__=function(){Bb(this.ptr)};C.prototype=Object.create(v.prototype);C.prototype.constructor=C;C.prototype.__class__=C;C.__cache__={};a.Status=C;C.prototype.code=
|
||||
C.prototype.code=function(){return Cb(this.ptr)};C.prototype.ok=C.prototype.ok=function(){return!!Db(this.ptr)};C.prototype.error_msg=C.prototype.error_msg=function(){return p(Eb(this.ptr))};C.prototype.__destroy__=C.prototype.__destroy__=function(){Fb(this.ptr)};I.prototype=Object.create(v.prototype);I.prototype.constructor=I;I.prototype.__class__=I;I.__cache__={};a.DracoFloat32Array=I;I.prototype.GetValue=I.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Gb(c,
|
||||
b)};I.prototype.size=I.prototype.size=function(){return Hb(this.ptr)};I.prototype.__destroy__=I.prototype.__destroy__=function(){Ib(this.ptr)};J.prototype=Object.create(v.prototype);J.prototype.constructor=J;J.prototype.__class__=J;J.__cache__={};a.DracoInt8Array=J;J.prototype.GetValue=J.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Jb(c,b)};J.prototype.size=J.prototype.size=function(){return Kb(this.ptr)};J.prototype.__destroy__=J.prototype.__destroy__=function(){Lb(this.ptr)};
|
||||
K.prototype=Object.create(v.prototype);K.prototype.constructor=K;K.prototype.__class__=K;K.__cache__={};a.DracoUInt8Array=K;K.prototype.GetValue=K.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Mb(c,b)};K.prototype.size=K.prototype.size=function(){return Nb(this.ptr)};K.prototype.__destroy__=K.prototype.__destroy__=function(){Ob(this.ptr)};L.prototype=Object.create(v.prototype);L.prototype.constructor=L;L.prototype.__class__=L;L.__cache__={};a.DracoInt16Array=
|
||||
L;L.prototype.GetValue=L.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Pb(c,b)};L.prototype.size=L.prototype.size=function(){return Qb(this.ptr)};L.prototype.__destroy__=L.prototype.__destroy__=function(){Rb(this.ptr)};M.prototype=Object.create(v.prototype);M.prototype.constructor=M;M.prototype.__class__=M;M.__cache__={};a.DracoUInt16Array=M;M.prototype.GetValue=M.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Sb(c,b)};
|
||||
M.prototype.size=M.prototype.size=function(){return Tb(this.ptr)};M.prototype.__destroy__=M.prototype.__destroy__=function(){Ub(this.ptr)};N.prototype=Object.create(v.prototype);N.prototype.constructor=N;N.prototype.__class__=N;N.__cache__={};a.DracoInt32Array=N;N.prototype.GetValue=N.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Vb(c,b)};N.prototype.size=N.prototype.size=function(){return Wb(this.ptr)};N.prototype.__destroy__=N.prototype.__destroy__=function(){Xb(this.ptr)};
|
||||
O.prototype=Object.create(v.prototype);O.prototype.constructor=O;O.prototype.__class__=O;O.__cache__={};a.DracoUInt32Array=O;O.prototype.GetValue=O.prototype.GetValue=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Yb(c,b)};O.prototype.size=O.prototype.size=function(){return Zb(this.ptr)};O.prototype.__destroy__=O.prototype.__destroy__=function(){$b(this.ptr)};z.prototype=Object.create(v.prototype);z.prototype.constructor=z;z.prototype.__class__=z;z.__cache__={};a.MetadataQuerier=
|
||||
z;z.prototype.HasEntry=z.prototype.HasEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return!!ac(d,b,c)};z.prototype.GetIntEntry=z.prototype.GetIntEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return bc(d,b,c)};z.prototype.GetIntEntryArray=z.prototype.GetIntEntryArray=function(b,c,d){var g=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===
|
||||
typeof c?c.ptr:R(c);d&&"object"===typeof d&&(d=d.ptr);cc(g,b,c,d)};z.prototype.GetDoubleEntry=z.prototype.GetDoubleEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return dc(d,b,c)};z.prototype.GetStringEntry=z.prototype.GetStringEntry=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return p(ec(d,b,c))};z.prototype.NumEntries=z.prototype.NumEntries=function(b){var c=this.ptr;
|
||||
b&&"object"===typeof b&&(b=b.ptr);return fc(c,b)};z.prototype.GetEntryName=z.prototype.GetEntryName=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return p(gc(d,b,c))};z.prototype.__destroy__=z.prototype.__destroy__=function(){hc(this.ptr)};m.prototype=Object.create(v.prototype);m.prototype.constructor=m;m.prototype.__class__=m;m.__cache__={};a.Decoder=m;m.prototype.DecodeArrayToPointCloud=m.prototype.DecodeArrayToPointCloud=function(b,c,d){var g=
|
||||
this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return B(ic(g,b,c,d),C)};m.prototype.DecodeArrayToMesh=m.prototype.DecodeArrayToMesh=function(b,c,d){var g=this.ptr;r.prepare();"object"==typeof b&&(b=Z(b));c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return B(jc(g,b,c,d),C)};m.prototype.GetAttributeId=m.prototype.GetAttributeId=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&
|
||||
(c=c.ptr);return kc(d,b,c)};m.prototype.GetAttributeIdByName=m.prototype.GetAttributeIdByName=function(b,c){var d=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);return lc(d,b,c)};m.prototype.GetAttributeIdByMetadataEntry=m.prototype.GetAttributeIdByMetadataEntry=function(b,c,d){var g=this.ptr;r.prepare();b&&"object"===typeof b&&(b=b.ptr);c=c&&"object"===typeof c?c.ptr:R(c);d=d&&"object"===typeof d?d.ptr:R(d);return mc(g,b,c,d)};m.prototype.GetAttribute=
|
||||
m.prototype.GetAttribute=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(nc(d,b,c),x)};m.prototype.GetAttributeByUniqueId=m.prototype.GetAttributeByUniqueId=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(oc(d,b,c),x)};m.prototype.GetMetadata=m.prototype.GetMetadata=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return B(pc(c,b),T)};m.prototype.GetAttributeMetadata=m.prototype.GetAttributeMetadata=
|
||||
function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(qc(d,b,c),T)};m.prototype.GetFaceFromMesh=m.prototype.GetFaceFromMesh=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!rc(g,b,c,d)};m.prototype.GetTriangleStripsFromMesh=m.prototype.GetTriangleStripsFromMesh=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);
|
||||
return sc(d,b,c)};m.prototype.GetTrianglesUInt16Array=m.prototype.GetTrianglesUInt16Array=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!tc(g,b,c,d)};m.prototype.GetTrianglesUInt32Array=m.prototype.GetTrianglesUInt32Array=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!uc(g,b,c,d)};m.prototype.GetAttributeFloat=m.prototype.GetAttributeFloat=
|
||||
function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!vc(g,b,c,d)};m.prototype.GetAttributeFloatForAllPoints=m.prototype.GetAttributeFloatForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!wc(g,b,c,d)};m.prototype.GetAttributeIntForAllPoints=m.prototype.GetAttributeIntForAllPoints=function(b,c,d){var g=this.ptr;
|
||||
b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!xc(g,b,c,d)};m.prototype.GetAttributeInt8ForAllPoints=m.prototype.GetAttributeInt8ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!yc(g,b,c,d)};m.prototype.GetAttributeUInt8ForAllPoints=m.prototype.GetAttributeUInt8ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=
|
||||
b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!zc(g,b,c,d)};m.prototype.GetAttributeInt16ForAllPoints=m.prototype.GetAttributeInt16ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Ac(g,b,c,d)};m.prototype.GetAttributeUInt16ForAllPoints=m.prototype.GetAttributeUInt16ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&
|
||||
(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Bc(g,b,c,d)};m.prototype.GetAttributeInt32ForAllPoints=m.prototype.GetAttributeInt32ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);return!!Cc(g,b,c,d)};m.prototype.GetAttributeUInt32ForAllPoints=m.prototype.GetAttributeUInt32ForAllPoints=function(b,c,d){var g=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===
|
||||
typeof d&&(d=d.ptr);return!!Dc(g,b,c,d)};m.prototype.GetAttributeDataArrayForAllPoints=m.prototype.GetAttributeDataArrayForAllPoints=function(b,c,d,g,t){var aa=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);d&&"object"===typeof d&&(d=d.ptr);g&&"object"===typeof g&&(g=g.ptr);t&&"object"===typeof t&&(t=t.ptr);return!!Ec(aa,b,c,d,g,t)};m.prototype.SkipAttributeTransform=m.prototype.SkipAttributeTransform=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);Fc(c,
|
||||
b)};m.prototype.GetEncodedGeometryType_Deprecated=m.prototype.GetEncodedGeometryType_Deprecated=function(b){var c=this.ptr;b&&"object"===typeof b&&(b=b.ptr);return Gc(c,b)};m.prototype.DecodeBufferToPointCloud=m.prototype.DecodeBufferToPointCloud=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===typeof c&&(c=c.ptr);return B(Hc(d,b,c),C)};m.prototype.DecodeBufferToMesh=m.prototype.DecodeBufferToMesh=function(b,c){var d=this.ptr;b&&"object"===typeof b&&(b=b.ptr);c&&"object"===
|
||||
typeof c&&(c=c.ptr);return B(Ic(d,b,c),C)};m.prototype.__destroy__=m.prototype.__destroy__=function(){Jc(this.ptr)};(function(){function b(){a.ATTRIBUTE_INVALID_TRANSFORM=Kc();a.ATTRIBUTE_NO_TRANSFORM=Lc();a.ATTRIBUTE_QUANTIZATION_TRANSFORM=Mc();a.ATTRIBUTE_OCTAHEDRON_TRANSFORM=Nc();a.INVALID=Oc();a.POSITION=Pc();a.NORMAL=Qc();a.COLOR=Rc();a.TEX_COORD=Sc();a.GENERIC=Tc();a.INVALID_GEOMETRY_TYPE=Uc();a.POINT_CLOUD=Vc();a.TRIANGULAR_MESH=Wc();a.DT_INVALID=Xc();a.DT_INT8=Yc();a.DT_UINT8=Zc();a.DT_INT16=
|
||||
$c();a.DT_UINT16=ad();a.DT_INT32=bd();a.DT_UINT32=cd();a.DT_INT64=dd();a.DT_UINT64=ed();a.DT_FLOAT32=fd();a.DT_FLOAT64=gd();a.DT_BOOL=hd();a.DT_TYPES_COUNT=id();a.OK=jd();a.DRACO_ERROR=kd();a.IO_ERROR=ld();a.INVALID_PARAMETER=md();a.UNSUPPORTED_VERSION=nd();a.UNKNOWN_VERSION=od()}va?b():oa.unshift(b)})();if("function"===typeof a.onModuleParsed)a.onModuleParsed();a.Decoder.prototype.GetEncodedGeometryType=function(b){if(b.__class__&&b.__class__===a.DecoderBuffer)return a.Decoder.prototype.GetEncodedGeometryType_Deprecated(b);
|
||||
if(8>b.byteLength)return a.INVALID_GEOMETRY_TYPE;switch(b[7]){case 0:return a.POINT_CLOUD;case 1:return a.TRIANGULAR_MESH;default:return a.INVALID_GEOMETRY_TYPE}};return n.ready}}();"object"===typeof exports&&"object"===typeof module?module.exports=DracoDecoderModule:"function"===typeof define&&define.amd?define([],function(){return DracoDecoderModule}):"object"===typeof exports&&(exports.DracoDecoderModule=DracoDecoderModule);
|
||||
21
public/src/bblcdn/msc_basis_transcoder.js
Normal file
BIN
public/src/bblcdn/msc_basis_transcoder.wasm
Normal file
BIN
public/src/bblcdn/uastc_bc7.wasm
Normal file
BIN
public/src/bblcdn/zstddec.wasm
Normal file
BIN
public/src/hdr/bg03.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
public/src/hdr/bg2.jpg
Normal file
|
After Width: | Height: | Size: 867 KiB |
BIN
public/src/hdr/bg3.jpg
Normal file
|
After Width: | Height: | Size: 454 KiB |
BIN
public/src/hdr/environment (4).env
Normal file
BIN
public/src/hdr/sanGiuseppeBridge.env
Normal file
BIN
public/src/ui/背景.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
21
src/App.vue
Normal file
@ -0,0 +1,21 @@
|
||||
|
||||
<template>
|
||||
<Editor />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import MainApp from './view/MainApp.vue'
|
||||
import Editor from './view/Editor.vue'
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
288
src/api/README.md
Normal file
@ -0,0 +1,288 @@
|
||||
# API 架构重构说明
|
||||
|
||||
## 概述
|
||||
|
||||
本次重构将原有的网络请求代码进行了全面的重新设计和组织,提供了更加模块化、类型安全和易于扩展的API架构。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
src/api/
|
||||
├── types/ # 类型定义
|
||||
│ └── index.ts
|
||||
├── config/ # 配置文件
|
||||
│ └── index.ts
|
||||
├── utils/ # 工具函数
|
||||
│ └── index.ts
|
||||
├── core/ # 核心模块
|
||||
│ ├── request.ts # HTTP请求客户端
|
||||
│ ├── upload.ts # 文件上传模块
|
||||
│ └── download.ts # 文件下载模块
|
||||
├── modules/ # 业务API模块
|
||||
│ ├── editor.ts # 编辑器相关API
|
||||
│ └── resource.ts # 资源管理API
|
||||
├── examples/ # 使用示例
|
||||
│ └── usage.ts
|
||||
├── index.ts # 统一入口
|
||||
└── README.md # 说明文档
|
||||
```
|
||||
|
||||
## 主要特性
|
||||
|
||||
### 1. 类型安全
|
||||
- 完整的TypeScript类型定义
|
||||
- 泛型支持,确保请求和响应数据的类型安全
|
||||
- 严格的类型检查
|
||||
|
||||
### 2. 模块化设计
|
||||
- 核心功能与业务逻辑分离
|
||||
- 可插拔的模块设计
|
||||
- 易于扩展和维护
|
||||
|
||||
### 3. 文件上传功能
|
||||
- 普通文件上传
|
||||
- 图片上传(支持压缩)
|
||||
- 大文件分片上传
|
||||
- 批量文件上传
|
||||
- Base64上传
|
||||
- 上传进度监控
|
||||
|
||||
### 4. 文件下载功能
|
||||
- 普通文件下载
|
||||
- 分片下载(大文件)
|
||||
- 批量下载
|
||||
- 下载为不同格式(Blob、Base64、ArrayBuffer、Text)
|
||||
- 下载进度监控
|
||||
|
||||
### 5. 错误处理
|
||||
- 统一的错误处理机制
|
||||
- 自动重试功能
|
||||
- 详细的错误信息
|
||||
|
||||
### 6. 请求管理
|
||||
- 请求拦截器
|
||||
- 响应拦截器
|
||||
- 请求取消功能
|
||||
- 认证token管理
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 基本使用
|
||||
|
||||
```typescript
|
||||
import { editorApi, resourceApi, ApiManager } from '@/api'
|
||||
|
||||
// 设置认证token
|
||||
ApiManager.setAuthToken('your-token')
|
||||
|
||||
// 创建订单
|
||||
const order = await editorApi.createOrder({
|
||||
projectId: 'project-123',
|
||||
type: 'render'
|
||||
})
|
||||
|
||||
// 上传资源
|
||||
const resource = await resourceApi.uploadResource(file, {
|
||||
type: 'model',
|
||||
tags: ['建筑', '现代']
|
||||
})
|
||||
```
|
||||
|
||||
### 文件上传
|
||||
|
||||
```typescript
|
||||
import { fileUploader } from '@/api'
|
||||
|
||||
// 普通文件上传
|
||||
const result = await fileUploader.uploadFile({
|
||||
file,
|
||||
onProgress: (progress) => {
|
||||
console.log(`上传进度: ${progress}%`)
|
||||
}
|
||||
})
|
||||
|
||||
// 图片上传(带压缩)
|
||||
const imageResult = await fileUploader.uploadImage(file, {
|
||||
compress: true,
|
||||
quality: 0.8,
|
||||
maxWidth: 1920,
|
||||
maxHeight: 1080
|
||||
})
|
||||
|
||||
// 大文件分片上传
|
||||
const largeFileResult = await fileUploader.uploadLargeFile(file, {
|
||||
chunkSize: 2 * 1024 * 1024, // 2MB per chunk
|
||||
onProgress: (progress) => {
|
||||
console.log(`上传进度: ${progress}%`)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 文件下载
|
||||
|
||||
```typescript
|
||||
import { fileDownloader } from '@/api'
|
||||
|
||||
// 下载并保存文件
|
||||
await fileDownloader.downloadAndSave({
|
||||
url: 'https://example.com/file.zip',
|
||||
filename: 'download.zip'
|
||||
})
|
||||
|
||||
// 下载为Base64
|
||||
const base64 = await fileDownloader.downloadAsBase64({
|
||||
url: 'https://example.com/image.jpg'
|
||||
})
|
||||
|
||||
// 批量下载
|
||||
const result = await fileDownloader.downloadBatch([
|
||||
'https://example.com/file1.zip',
|
||||
'https://example.com/file2.zip'
|
||||
], {
|
||||
concurrent: 3,
|
||||
onProgress: (progress) => {
|
||||
console.log(`批量下载进度: ${progress}%`)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## API 模块
|
||||
|
||||
### 编辑器API (EditorApi)
|
||||
|
||||
提供编辑器相关的功能:
|
||||
|
||||
- `createOrder(params)` - 创建订单
|
||||
- `saveProject(projectData)` - 保存项目
|
||||
- `loadProject(projectId)` - 加载项目
|
||||
- `deleteProject(projectId)` - 删除项目
|
||||
- `getProjectList(params)` - 获取项目列表
|
||||
- `uploadThumbnail(file, projectId)` - 上传缩略图
|
||||
- `exportProject(projectId, format)` - 导出项目
|
||||
- `importProject(file)` - 导入项目
|
||||
|
||||
### 资源API (ResourceApi)
|
||||
|
||||
提供资源管理功能:
|
||||
|
||||
- `getResourceList(params)` - 获取资源列表
|
||||
- `getResourceDetail(resourceId)` - 获取资源详情
|
||||
- `uploadResource(file, metadata)` - 上传资源
|
||||
- `uploadMultipleResources(files, metadata)` - 批量上传资源
|
||||
- `deleteResource(resourceId)` - 删除资源
|
||||
- `downloadResource(resource)` - 下载资源
|
||||
- `searchResources(params)` - 搜索资源
|
||||
|
||||
## 配置
|
||||
|
||||
### 环境变量
|
||||
|
||||
```bash
|
||||
# .env.development
|
||||
VITE_BASE_URL=http://localhost:3000/api
|
||||
|
||||
# .env.production
|
||||
VITE_BASE_URL=https://api.example.com
|
||||
```
|
||||
|
||||
### API配置
|
||||
|
||||
```typescript
|
||||
// src/api/config/index.ts
|
||||
export const API_CONFIG = {
|
||||
BASE_URL: import.meta.env.VITE_BASE_URL,
|
||||
TIMEOUT: 15000,
|
||||
RETRY_COUNT: 3,
|
||||
RETRY_DELAY: 1000,
|
||||
// ...其他配置
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
API会自动处理常见的错误情况:
|
||||
|
||||
- 网络连接失败
|
||||
- 请求超时
|
||||
- 认证失败
|
||||
- 权限不足
|
||||
- 服务器错误
|
||||
|
||||
所有API方法都返回统一的响应格式:
|
||||
|
||||
```typescript
|
||||
interface ApiResponse<T> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
success: boolean
|
||||
}
|
||||
```
|
||||
|
||||
## 扩展性
|
||||
|
||||
### 添加新的API模块
|
||||
|
||||
1. 在 `src/api/modules/` 目录下创建新的模块文件
|
||||
2. 定义相关的类型接口
|
||||
3. 实现API类
|
||||
4. 在 `src/api/index.ts` 中导出
|
||||
|
||||
### 添加新的请求拦截器
|
||||
|
||||
```typescript
|
||||
import { httpClient } from '@/api'
|
||||
|
||||
// 添加请求拦截器
|
||||
httpClient.instance.interceptors.request.use(
|
||||
(config) => {
|
||||
// 自定义请求处理
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## 迁移指南
|
||||
|
||||
### 从旧API迁移
|
||||
|
||||
旧的API调用:
|
||||
```typescript
|
||||
// 旧方式
|
||||
const request = new Request()
|
||||
request.post(new ParamsReq('/api/endpoint', data))
|
||||
```
|
||||
|
||||
新的API调用:
|
||||
```typescript
|
||||
// 新方式
|
||||
import { httpClient } from '@/api'
|
||||
const response = await httpClient.post('/api/endpoint', data)
|
||||
```
|
||||
|
||||
### 兼容性
|
||||
|
||||
新的API架构与旧代码完全分离,可以逐步迁移,不会影响现有功能。
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用TypeScript类型**:充分利用类型定义,确保代码的类型安全
|
||||
2. **错误处理**:始终处理API调用可能出现的错误
|
||||
3. **进度监控**:对于文件上传下载,使用进度回调提升用户体验
|
||||
4. **请求取消**:在组件卸载时取消未完成的请求
|
||||
5. **缓存策略**:对于不经常变化的数据,考虑实现缓存机制
|
||||
|
||||
## 性能优化
|
||||
|
||||
1. **并发控制**:批量操作时控制并发数量
|
||||
2. **分片上传**:大文件使用分片上传
|
||||
3. **压缩优化**:图片上传时自动压缩
|
||||
4. **请求去重**:避免重复请求
|
||||
5. **缓存机制**:合理使用缓存减少网络请求
|
||||
|
||||
## 示例代码
|
||||
|
||||
详细的使用示例请参考 `src/api/examples/usage.ts` 文件。
|
||||
83
src/api/config/index.ts
Normal file
@ -0,0 +1,83 @@
|
||||
// API 基础配置
|
||||
export const API_CONFIG = {
|
||||
// 基础URL
|
||||
BASE_URL: import.meta.env.VITE_BASE_URL || 'https://script.jiaweijia.cn/script',
|
||||
|
||||
// 超时时间
|
||||
TIMEOUT: 15000,
|
||||
|
||||
// 重试次数
|
||||
RETRY_COUNT: 3,
|
||||
|
||||
// 重试延迟(毫秒)
|
||||
RETRY_DELAY: 1000,
|
||||
|
||||
// 默认请求头
|
||||
DEFAULT_HEADERS: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
|
||||
// 文件上传请求头
|
||||
UPLOAD_HEADERS: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
|
||||
// 响应状态码
|
||||
STATUS_CODES: {
|
||||
SUCCESS: 200,
|
||||
UNAUTHORIZED: 401,
|
||||
FORBIDDEN: 403,
|
||||
NOT_FOUND: 404,
|
||||
SERVER_ERROR: 500,
|
||||
},
|
||||
|
||||
// 业务状态码
|
||||
BUSINESS_CODES: {
|
||||
SUCCESS: 0,
|
||||
FAIL: -1,
|
||||
TOKEN_EXPIRED: 401,
|
||||
PERMISSION_DENIED: 403,
|
||||
}
|
||||
}
|
||||
|
||||
// 环境配置
|
||||
export const ENV_CONFIG = {
|
||||
isDev: import.meta.env.DEV,
|
||||
isProd: import.meta.env.PROD,
|
||||
mode: import.meta.env.MODE,
|
||||
}
|
||||
|
||||
// API 端点配置
|
||||
export const API_ENDPOINTS = {
|
||||
// 编辑器相关
|
||||
EDITOR: {
|
||||
CREATE_ORDER: '/cpq/3d/create/order',
|
||||
SAVE_PROJECT: '/editor/project/save',
|
||||
LOAD_PROJECT: '/editor/project/load',
|
||||
DELETE_PROJECT: '/editor/project/delete',
|
||||
},
|
||||
|
||||
// 文件相关
|
||||
FILE: {
|
||||
UPLOAD: '/file/upload',
|
||||
DOWNLOAD: '/file/download',
|
||||
DELETE: '/file/delete',
|
||||
LIST: '/file/list',
|
||||
},
|
||||
|
||||
// 用户相关
|
||||
USER: {
|
||||
LOGIN: '/user/login',
|
||||
LOGOUT: '/user/logout',
|
||||
INFO: '/user/info',
|
||||
UPDATE: '/user/update',
|
||||
},
|
||||
|
||||
// 资源相关
|
||||
RESOURCE: {
|
||||
LIST: '/resource/list',
|
||||
UPLOAD: '/resource/upload',
|
||||
DELETE: '/resource/delete',
|
||||
}
|
||||
}
|
||||
377
src/api/core/download.ts
Normal file
@ -0,0 +1,377 @@
|
||||
import { httpClient } from './request'
|
||||
import { DownloadConfig, ApiResponse } from '../types'
|
||||
import { API_ENDPOINTS } from '../config'
|
||||
import { downloadFile, generateId } from '../utils'
|
||||
|
||||
/**
|
||||
* 下载响应类型
|
||||
*/
|
||||
export interface DownloadResponse {
|
||||
blob: Blob
|
||||
filename: string
|
||||
size: number
|
||||
type: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载响应类型
|
||||
*/
|
||||
export interface BatchDownloadResponse {
|
||||
success: DownloadResponse[]
|
||||
failed: Array<{
|
||||
url: string
|
||||
error: string
|
||||
}>
|
||||
total: number
|
||||
successCount: number
|
||||
failedCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件下载类
|
||||
*/
|
||||
export class FileDownloader {
|
||||
private downloadQueue: Map<string, AbortController> = new Map()
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
public async downloadFile(config: DownloadConfig): Promise<DownloadResponse> {
|
||||
const {
|
||||
url,
|
||||
filename,
|
||||
params,
|
||||
onProgress
|
||||
} = config
|
||||
|
||||
// 生成下载ID
|
||||
const downloadId = generateId()
|
||||
const controller = new AbortController()
|
||||
this.downloadQueue.set(downloadId, controller)
|
||||
|
||||
try {
|
||||
const response = await httpClient.request<Blob>({
|
||||
url,
|
||||
method: 'GET',
|
||||
params,
|
||||
responseType: 'blob',
|
||||
timeout: 0, // 下载不设置超时
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.message)
|
||||
}
|
||||
|
||||
const blob = response.data
|
||||
const contentType = blob.type || 'application/octet-stream'
|
||||
|
||||
// 从响应头或URL中获取文件名
|
||||
const finalFilename = filename || this.extractFilenameFromUrl(url) || 'download'
|
||||
|
||||
return {
|
||||
blob,
|
||||
filename: finalFilename,
|
||||
size: blob.size,
|
||||
type: contentType
|
||||
}
|
||||
} finally {
|
||||
this.downloadQueue.delete(downloadId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载并保存文件
|
||||
*/
|
||||
public async downloadAndSave(config: DownloadConfig): Promise<void> {
|
||||
const result = await this.downloadFile(config)
|
||||
downloadFile(result.blob, result.filename)
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件为Base64
|
||||
*/
|
||||
public async downloadAsBase64(config: DownloadConfig): Promise<string> {
|
||||
const result = await this.downloadFile(config)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const base64 = reader.result as string
|
||||
resolve(base64.split(',')[1]) // 移除data:xxx;base64,前缀
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(result.blob)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件为ArrayBuffer
|
||||
*/
|
||||
public async downloadAsArrayBuffer(config: DownloadConfig): Promise<ArrayBuffer> {
|
||||
const result = await this.downloadFile(config)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(reader.result as ArrayBuffer)
|
||||
reader.onerror = reject
|
||||
reader.readAsArrayBuffer(result.blob)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件为文本
|
||||
*/
|
||||
public async downloadAsText(config: DownloadConfig): Promise<string> {
|
||||
const result = await this.downloadFile(config)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(reader.result as string)
|
||||
reader.onerror = reject
|
||||
reader.readAsText(result.blob)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 分片下载(大文件)
|
||||
*/
|
||||
public async downloadLargeFile(
|
||||
config: DownloadConfig & {
|
||||
chunkSize?: number
|
||||
onChunkProgress?: (chunkIndex: number, chunkProgress: number) => void
|
||||
}
|
||||
): Promise<DownloadResponse> {
|
||||
const {
|
||||
url,
|
||||
filename,
|
||||
params,
|
||||
chunkSize = 2 * 1024 * 1024, // 2MB per chunk
|
||||
onProgress,
|
||||
onChunkProgress
|
||||
} = config
|
||||
|
||||
try {
|
||||
// 获取文件信息
|
||||
const headResponse = await httpClient.request({
|
||||
url,
|
||||
method: 'HEAD',
|
||||
params
|
||||
})
|
||||
|
||||
if (!headResponse.success) {
|
||||
throw new Error('无法获取文件信息')
|
||||
}
|
||||
|
||||
// 这里需要根据实际的响应头来获取文件大小
|
||||
// const fileSize = parseInt(headResponse.headers['content-length'] || '0')
|
||||
const fileSize = 0 // 临时设置,实际应该从响应头获取
|
||||
|
||||
if (fileSize === 0) {
|
||||
// 如果无法获取文件大小,回退到普通下载
|
||||
return this.downloadFile(config)
|
||||
}
|
||||
|
||||
const totalChunks = Math.ceil(fileSize / chunkSize)
|
||||
const chunks: Blob[] = []
|
||||
|
||||
// 下载分片
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
const start = i * chunkSize
|
||||
const end = Math.min(start + chunkSize - 1, fileSize - 1)
|
||||
|
||||
const chunkResponse = await httpClient.request<Blob>({
|
||||
url,
|
||||
method: 'GET',
|
||||
params,
|
||||
headers: {
|
||||
'Range': `bytes=${start}-${end}`
|
||||
},
|
||||
responseType: 'blob'
|
||||
})
|
||||
|
||||
if (chunkResponse.success) {
|
||||
chunks.push(chunkResponse.data)
|
||||
|
||||
// 分片进度回调
|
||||
onChunkProgress?.(i, 100)
|
||||
|
||||
// 总进度回调
|
||||
const progress = ((i + 1) / totalChunks) * 100
|
||||
onProgress?.(progress)
|
||||
} else {
|
||||
throw new Error(`分片 ${i} 下载失败: ${chunkResponse.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 合并分片
|
||||
const blob = new Blob(chunks)
|
||||
const finalFilename = filename || this.extractFilenameFromUrl(url) || 'download'
|
||||
|
||||
return {
|
||||
blob,
|
||||
filename: finalFilename,
|
||||
size: blob.size,
|
||||
type: blob.type || 'application/octet-stream'
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`分片下载失败: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载
|
||||
*/
|
||||
public async downloadBatch(
|
||||
urls: string[],
|
||||
options: {
|
||||
concurrent?: number
|
||||
onProgress?: (progress: number) => void
|
||||
onFileProgress?: (url: string, progress: number) => void
|
||||
onFileComplete?: (url: string, result: DownloadResponse | Error) => void
|
||||
} = {}
|
||||
): Promise<BatchDownloadResponse> {
|
||||
const {
|
||||
concurrent = 3,
|
||||
onProgress,
|
||||
onFileProgress,
|
||||
onFileComplete
|
||||
} = options
|
||||
|
||||
const results: BatchDownloadResponse = {
|
||||
success: [],
|
||||
failed: [],
|
||||
total: urls.length,
|
||||
successCount: 0,
|
||||
failedCount: 0
|
||||
}
|
||||
|
||||
const downloadUrl = async (url: string): Promise<void> => {
|
||||
try {
|
||||
const response = await this.downloadFile({
|
||||
url,
|
||||
onProgress: (progress) => onFileProgress?.(url, progress)
|
||||
})
|
||||
|
||||
results.success.push(response)
|
||||
results.successCount++
|
||||
onFileComplete?.(url, response)
|
||||
} catch (error) {
|
||||
results.failed.push({
|
||||
url,
|
||||
error: error instanceof Error ? error.message : '下载失败'
|
||||
})
|
||||
results.failedCount++
|
||||
onFileComplete?.(url, error as Error)
|
||||
}
|
||||
|
||||
// 更新总进度
|
||||
const completed = results.successCount + results.failedCount
|
||||
const progress = (completed / results.total) * 100
|
||||
onProgress?.(progress)
|
||||
}
|
||||
|
||||
// 并发下载
|
||||
const chunks = []
|
||||
for (let i = 0; i < urls.length; i += concurrent) {
|
||||
chunks.push(urls.slice(i, i + concurrent))
|
||||
}
|
||||
|
||||
for (const chunk of chunks) {
|
||||
await Promise.all(chunk.map(downloadUrl))
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载并打包为ZIP
|
||||
*/
|
||||
public async downloadAsZip(
|
||||
files: Array<{
|
||||
url: string
|
||||
filename?: string
|
||||
}>,
|
||||
zipFilename: string = 'download.zip'
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 下载所有文件
|
||||
const downloadPromises = files.map(async (file) => {
|
||||
const result = await this.downloadFile({
|
||||
url: file.url,
|
||||
filename: file.filename
|
||||
})
|
||||
return {
|
||||
filename: result.filename,
|
||||
blob: result.blob
|
||||
}
|
||||
})
|
||||
|
||||
const downloadedFiles = await Promise.all(downloadPromises)
|
||||
|
||||
// 这里需要使用JSZip库来创建ZIP文件
|
||||
// 由于没有导入JSZip,这里只是示例代码
|
||||
console.log('需要集成JSZip库来实现ZIP打包功能')
|
||||
console.log('下载的文件:', downloadedFiles)
|
||||
|
||||
// 示例:如果有JSZip
|
||||
// const JSZip = (await import('jszip')).default
|
||||
// const zip = new JSZip()
|
||||
//
|
||||
// downloadedFiles.forEach(file => {
|
||||
// zip.file(file.filename, file.blob)
|
||||
// })
|
||||
//
|
||||
// const zipBlob = await zip.generateAsync({ type: 'blob' })
|
||||
// downloadFile(zipBlob, zipFilename)
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`ZIP下载失败: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL中提取文件名
|
||||
*/
|
||||
private extractFilenameFromUrl(url: string): string {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
const pathname = urlObj.pathname
|
||||
const filename = pathname.split('/').pop()
|
||||
return filename || 'download'
|
||||
} catch {
|
||||
return 'download'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消下载
|
||||
*/
|
||||
public cancelDownload(downloadId: string): void {
|
||||
const controller = this.downloadQueue.get(downloadId)
|
||||
if (controller) {
|
||||
controller.abort()
|
||||
this.downloadQueue.delete(downloadId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有下载
|
||||
*/
|
||||
public cancelAllDownloads(): void {
|
||||
this.downloadQueue.forEach((controller) => {
|
||||
controller.abort()
|
||||
})
|
||||
this.downloadQueue.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载进度
|
||||
*/
|
||||
public getDownloadProgress(downloadId: string): number {
|
||||
// 这里可以实现获取具体下载进度的逻辑
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// 创建默认实例
|
||||
export const fileDownloader = new FileDownloader()
|
||||
344
src/api/core/request.ts
Normal file
@ -0,0 +1,344 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
|
||||
import { ApiResponse, RequestConfig, ApiError } from '../types'
|
||||
import { API_CONFIG, ENV_CONFIG } from '../config'
|
||||
import { createApiError, retry } from '../utils'
|
||||
|
||||
/**
|
||||
* HTTP请求客户端类
|
||||
*/
|
||||
export class HttpClient {
|
||||
private instance: AxiosInstance
|
||||
private requestQueue: Map<string, AbortController> = new Map()
|
||||
|
||||
constructor() {
|
||||
this.instance = this.createInstance()
|
||||
this.setupInterceptors()
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建axios实例
|
||||
*/
|
||||
private createInstance(): AxiosInstance {
|
||||
return axios.create({
|
||||
baseURL: API_CONFIG.BASE_URL,
|
||||
timeout: API_CONFIG.TIMEOUT,
|
||||
headers: API_CONFIG.DEFAULT_HEADERS,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置拦截器
|
||||
*/
|
||||
private setupInterceptors(): void {
|
||||
// 请求拦截器
|
||||
this.instance.interceptors.request.use(
|
||||
(config) => {
|
||||
// 添加请求ID用于取消请求
|
||||
const requestId = this.generateRequestId(config)
|
||||
config.metadata = { requestId }
|
||||
|
||||
// 添加认证token
|
||||
const token = this.getAuthToken()
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
// 开发环境下打印请求信息
|
||||
if (ENV_CONFIG.isDev) {
|
||||
console.log('🚀 Request:', {
|
||||
url: config.url,
|
||||
method: config.method,
|
||||
params: config.params,
|
||||
data: config.data,
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(this.handleError(error))
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
this.instance.interceptors.response.use(
|
||||
(response) => {
|
||||
// 清理请求队列
|
||||
const requestId = response.config.metadata?.requestId
|
||||
if (requestId) {
|
||||
this.requestQueue.delete(requestId)
|
||||
}
|
||||
|
||||
// 开发环境下打印响应信息
|
||||
if (ENV_CONFIG.isDev) {
|
||||
console.log('✅ Response:', {
|
||||
url: response.config.url,
|
||||
status: response.status,
|
||||
data: response.data,
|
||||
})
|
||||
}
|
||||
|
||||
// 直接返回response,在具体的请求方法中处理数据格式
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
// 清理请求队列
|
||||
const requestId = error.config?.metadata?.requestId
|
||||
if (requestId) {
|
||||
this.requestQueue.delete(requestId)
|
||||
}
|
||||
|
||||
return Promise.reject(this.handleError(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理响应数据
|
||||
*/
|
||||
private handleResponse<T>(response: AxiosResponse): ApiResponse<T> {
|
||||
const { data } = response
|
||||
|
||||
// 如果后端返回的数据已经是标准格式
|
||||
if (data && typeof data.code !== 'undefined') {
|
||||
return {
|
||||
code: data.code,
|
||||
message: data.message || 'Success',
|
||||
data: data.data,
|
||||
success: data.code === API_CONFIG.BUSINESS_CODES.SUCCESS,
|
||||
}
|
||||
}
|
||||
|
||||
// 否则包装成标准格式
|
||||
return {
|
||||
code: API_CONFIG.BUSINESS_CODES.SUCCESS,
|
||||
message: 'Success',
|
||||
data: data,
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理错误
|
||||
*/
|
||||
private handleError(error: any): ApiError {
|
||||
if (ENV_CONFIG.isDev) {
|
||||
console.error('❌ Request Error:', error)
|
||||
}
|
||||
|
||||
// 请求被取消
|
||||
if (axios.isCancel(error)) {
|
||||
return createApiError(-1, '请求已取消')
|
||||
}
|
||||
|
||||
// 网络错误
|
||||
if (!error.response) {
|
||||
return createApiError(-1, '网络连接失败,请检查网络设置')
|
||||
}
|
||||
|
||||
const { status, data } = error.response as AxiosResponse
|
||||
|
||||
// 根据HTTP状态码处理
|
||||
switch (status) {
|
||||
case API_CONFIG.STATUS_CODES.UNAUTHORIZED:
|
||||
this.handleUnauthorized()
|
||||
return createApiError(status, '登录已过期,请重新登录')
|
||||
|
||||
case API_CONFIG.STATUS_CODES.FORBIDDEN:
|
||||
return createApiError(status, '没有权限访问该资源')
|
||||
|
||||
case API_CONFIG.STATUS_CODES.NOT_FOUND:
|
||||
return createApiError(status, '请求的资源不存在')
|
||||
|
||||
case API_CONFIG.STATUS_CODES.SERVER_ERROR:
|
||||
return createApiError(status, '服务器内部错误')
|
||||
|
||||
default:
|
||||
return createApiError(
|
||||
status,
|
||||
data?.message || `请求失败 (${status})`,
|
||||
data
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理未授权
|
||||
*/
|
||||
private handleUnauthorized(): void {
|
||||
// 清除token
|
||||
this.clearAuthToken()
|
||||
|
||||
// 可以在这里添加跳转到登录页的逻辑
|
||||
// router.push('/login')
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成请求ID
|
||||
*/
|
||||
private generateRequestId(config: AxiosRequestConfig): string {
|
||||
return `${config.method}_${config.url}_${Date.now()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取认证token
|
||||
*/
|
||||
private getAuthToken(): string | null {
|
||||
return localStorage.getItem('auth_token')
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除认证token
|
||||
*/
|
||||
private clearAuthToken(): void {
|
||||
localStorage.removeItem('auth_token')
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置认证token
|
||||
*/
|
||||
public setAuthToken(token: string): void {
|
||||
localStorage.setItem('auth_token', token)
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用请求方法
|
||||
*/
|
||||
public async request<T = any>(config: RequestConfig): Promise<ApiResponse<T>> {
|
||||
const axiosConfig: AxiosRequestConfig = {
|
||||
url: config.url,
|
||||
method: config.method || 'GET',
|
||||
params: config.params,
|
||||
data: config.data,
|
||||
headers: config.headers,
|
||||
timeout: config.timeout,
|
||||
responseType: config.responseType || 'json',
|
||||
}
|
||||
|
||||
const response = await this.instance.request(axiosConfig)
|
||||
return this.handleResponse<T>(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* 带重试的请求方法
|
||||
*/
|
||||
public async requestWithRetry<T = any>(
|
||||
config: RequestConfig,
|
||||
retries?: number
|
||||
): Promise<ApiResponse<T>> {
|
||||
return retry(() => this.request<T>(config), retries)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET请求
|
||||
*/
|
||||
public get<T = any>(
|
||||
url: string,
|
||||
params?: Record<string, any>,
|
||||
config?: Partial<RequestConfig>
|
||||
): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'GET',
|
||||
params,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* POST请求
|
||||
*/
|
||||
public post<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: Partial<RequestConfig>
|
||||
): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'POST',
|
||||
data,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT请求
|
||||
*/
|
||||
public put<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: Partial<RequestConfig>
|
||||
): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'PUT',
|
||||
data,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE请求
|
||||
*/
|
||||
public delete<T = any>(
|
||||
url: string,
|
||||
params?: Record<string, any>,
|
||||
config?: Partial<RequestConfig>
|
||||
): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'DELETE',
|
||||
params,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH请求
|
||||
*/
|
||||
public patch<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: Partial<RequestConfig>
|
||||
): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'PATCH',
|
||||
data,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消请求
|
||||
*/
|
||||
public cancelRequest(requestId: string): void {
|
||||
const controller = this.requestQueue.get(requestId)
|
||||
if (controller) {
|
||||
controller.abort()
|
||||
this.requestQueue.delete(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有请求
|
||||
*/
|
||||
public cancelAllRequests(): void {
|
||||
this.requestQueue.forEach((controller) => {
|
||||
controller.abort()
|
||||
})
|
||||
this.requestQueue.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// 创建默认实例
|
||||
export const httpClient = new HttpClient()
|
||||
|
||||
// 扩展axios配置类型
|
||||
declare module 'axios' {
|
||||
interface AxiosRequestConfig {
|
||||
metadata?: {
|
||||
requestId: string
|
||||
}
|
||||
}
|
||||
}
|
||||
362
src/api/core/upload.ts
Normal file
@ -0,0 +1,362 @@
|
||||
import { AxiosRequestConfig } from 'axios'
|
||||
import { httpClient } from './request'
|
||||
import { UploadConfig, ApiResponse } from '../types'
|
||||
import { API_CONFIG, API_ENDPOINTS } from '../config'
|
||||
import {
|
||||
validateFileType,
|
||||
formatFileSize,
|
||||
compressImage,
|
||||
readFileAsBase64,
|
||||
generateId
|
||||
} from '../utils'
|
||||
|
||||
/**
|
||||
* 文件上传响应类型
|
||||
*/
|
||||
export interface UploadResponse {
|
||||
fileId: string
|
||||
filename: string
|
||||
url: string
|
||||
size: number
|
||||
type: string
|
||||
uploadTime: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上传响应类型
|
||||
*/
|
||||
export interface BatchUploadResponse {
|
||||
success: UploadResponse[]
|
||||
failed: Array<{
|
||||
file: File
|
||||
error: string
|
||||
}>
|
||||
total: number
|
||||
successCount: number
|
||||
failedCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传类
|
||||
*/
|
||||
export class FileUploader {
|
||||
private uploadQueue: Map<string, AbortController> = new Map()
|
||||
|
||||
/**
|
||||
* 单文件上传
|
||||
*/
|
||||
public async uploadFile(config: UploadConfig): Promise<ApiResponse<UploadResponse>> {
|
||||
const {
|
||||
file,
|
||||
filename = file instanceof File ? file.name : 'blob',
|
||||
fieldName = 'file',
|
||||
data = {},
|
||||
onProgress
|
||||
} = config
|
||||
|
||||
// 创建FormData
|
||||
const formData = new FormData()
|
||||
formData.append(fieldName, file, filename)
|
||||
|
||||
// 添加额外数据
|
||||
Object.keys(data).forEach(key => {
|
||||
formData.append(key, data[key])
|
||||
})
|
||||
|
||||
// 生成上传ID
|
||||
const uploadId = generateId()
|
||||
const controller = new AbortController()
|
||||
this.uploadQueue.set(uploadId, controller)
|
||||
|
||||
try {
|
||||
const response = await httpClient.request<UploadResponse>({
|
||||
url: API_ENDPOINTS.FILE.UPLOAD,
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
timeout: 0, // 上传不设置超时
|
||||
responseType: 'json',
|
||||
})
|
||||
|
||||
return response
|
||||
} finally {
|
||||
this.uploadQueue.delete(uploadId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片上传(带压缩)
|
||||
*/
|
||||
public async uploadImage(
|
||||
file: File,
|
||||
options: {
|
||||
compress?: boolean
|
||||
quality?: number
|
||||
maxWidth?: number
|
||||
maxHeight?: number
|
||||
allowedTypes?: string[]
|
||||
maxSize?: number
|
||||
onProgress?: (progress: number) => void
|
||||
} = {}
|
||||
): Promise<ApiResponse<UploadResponse>> {
|
||||
const {
|
||||
compress = true,
|
||||
quality = 0.8,
|
||||
maxWidth = 1920,
|
||||
maxHeight = 1080,
|
||||
allowedTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp'],
|
||||
maxSize = 10 * 1024 * 1024, // 10MB
|
||||
onProgress
|
||||
} = options
|
||||
|
||||
// 验证文件类型
|
||||
if (!validateFileType(file, allowedTypes)) {
|
||||
throw new Error(`不支持的文件类型,仅支持: ${allowedTypes.join(', ')}`)
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
if (file.size > maxSize) {
|
||||
throw new Error(`文件大小超出限制,最大支持: ${formatFileSize(maxSize)}`)
|
||||
}
|
||||
|
||||
let uploadFile = file
|
||||
|
||||
// 压缩图片
|
||||
if (compress && file.type.startsWith('image/')) {
|
||||
try {
|
||||
const compressedBlob = await compressImage(file, quality, maxWidth, maxHeight)
|
||||
uploadFile = new File([compressedBlob], file.name, { type: file.type })
|
||||
} catch (error) {
|
||||
console.warn('图片压缩失败,使用原文件上传:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return this.uploadFile({
|
||||
file: uploadFile,
|
||||
onProgress
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64上传
|
||||
*/
|
||||
public async uploadBase64(
|
||||
file: File,
|
||||
options: {
|
||||
onProgress?: (progress: number) => void
|
||||
} = {}
|
||||
): Promise<ApiResponse<UploadResponse>> {
|
||||
const { onProgress } = options
|
||||
|
||||
try {
|
||||
const base64 = await readFileAsBase64(file)
|
||||
|
||||
const response = await httpClient.post<UploadResponse>(
|
||||
API_ENDPOINTS.FILE.UPLOAD,
|
||||
{
|
||||
filename: file.name,
|
||||
content: base64,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
encoding: 'base64'
|
||||
}
|
||||
)
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
throw new Error(`Base64上传失败: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分片上传(大文件)
|
||||
*/
|
||||
public async uploadLargeFile(
|
||||
file: File,
|
||||
options: {
|
||||
chunkSize?: number
|
||||
onProgress?: (progress: number) => void
|
||||
onChunkProgress?: (chunkIndex: number, chunkProgress: number) => void
|
||||
} = {}
|
||||
): Promise<ApiResponse<UploadResponse>> {
|
||||
const {
|
||||
chunkSize = 2 * 1024 * 1024, // 2MB per chunk
|
||||
onProgress,
|
||||
onChunkProgress
|
||||
} = options
|
||||
|
||||
const totalChunks = Math.ceil(file.size / chunkSize)
|
||||
const uploadId = generateId()
|
||||
const uploadedChunks: string[] = []
|
||||
|
||||
try {
|
||||
// 初始化分片上传
|
||||
const initResponse = await httpClient.post('/file/upload/init', {
|
||||
filename: file.name,
|
||||
fileSize: file.size,
|
||||
totalChunks,
|
||||
uploadId
|
||||
})
|
||||
|
||||
if (!initResponse.success) {
|
||||
throw new Error(initResponse.message)
|
||||
}
|
||||
|
||||
// 上传分片
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
const start = i * chunkSize
|
||||
const end = Math.min(start + chunkSize, file.size)
|
||||
const chunk = file.slice(start, end)
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('chunk', chunk)
|
||||
formData.append('chunkIndex', i.toString())
|
||||
formData.append('uploadId', uploadId)
|
||||
|
||||
const chunkResponse = await httpClient.post('/file/upload/chunk', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
}
|
||||
})
|
||||
|
||||
if (chunkResponse.success) {
|
||||
uploadedChunks.push(chunkResponse.data.chunkId)
|
||||
|
||||
// 分片进度回调
|
||||
onChunkProgress?.(i, 100)
|
||||
|
||||
// 总进度回调
|
||||
const progress = ((i + 1) / totalChunks) * 100
|
||||
onProgress?.(progress)
|
||||
} else {
|
||||
throw new Error(`分片 ${i} 上传失败: ${chunkResponse.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 合并分片
|
||||
const mergeResponse = await httpClient.post<UploadResponse>('/file/upload/merge', {
|
||||
uploadId,
|
||||
filename: file.name,
|
||||
chunks: uploadedChunks
|
||||
})
|
||||
|
||||
return mergeResponse
|
||||
} catch (error) {
|
||||
// 清理失败的上传
|
||||
try {
|
||||
await httpClient.post('/file/upload/cleanup', { uploadId })
|
||||
} catch (cleanupError) {
|
||||
console.warn('清理上传失败:', cleanupError)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上传
|
||||
*/
|
||||
public async uploadBatch(
|
||||
files: File[],
|
||||
options: {
|
||||
concurrent?: number
|
||||
onProgress?: (progress: number) => void
|
||||
onFileProgress?: (file: File, progress: number) => void
|
||||
onFileComplete?: (file: File, result: UploadResponse | Error) => void
|
||||
} = {}
|
||||
): Promise<BatchUploadResponse> {
|
||||
const {
|
||||
concurrent = 3,
|
||||
onProgress,
|
||||
onFileProgress,
|
||||
onFileComplete
|
||||
} = options
|
||||
|
||||
const results: BatchUploadResponse = {
|
||||
success: [],
|
||||
failed: [],
|
||||
total: files.length,
|
||||
successCount: 0,
|
||||
failedCount: 0
|
||||
}
|
||||
|
||||
const uploadFile = async (file: File): Promise<void> => {
|
||||
try {
|
||||
const response = await this.uploadFile({
|
||||
file,
|
||||
onProgress: (progress) => onFileProgress?.(file, progress)
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
results.success.push(response.data)
|
||||
results.successCount++
|
||||
} else {
|
||||
results.failed.push({
|
||||
file,
|
||||
error: response.message
|
||||
})
|
||||
results.failedCount++
|
||||
}
|
||||
|
||||
onFileComplete?.(file, response.data)
|
||||
} catch (error) {
|
||||
results.failed.push({
|
||||
file,
|
||||
error: error instanceof Error ? error.message : '上传失败'
|
||||
})
|
||||
results.failedCount++
|
||||
onFileComplete?.(file, error as Error)
|
||||
}
|
||||
|
||||
// 更新总进度
|
||||
const completed = results.successCount + results.failedCount
|
||||
const progress = (completed / results.total) * 100
|
||||
onProgress?.(progress)
|
||||
}
|
||||
|
||||
// 并发上传
|
||||
const chunks = []
|
||||
for (let i = 0; i < files.length; i += concurrent) {
|
||||
chunks.push(files.slice(i, i + concurrent))
|
||||
}
|
||||
|
||||
for (const chunk of chunks) {
|
||||
await Promise.all(chunk.map(uploadFile))
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消上传
|
||||
*/
|
||||
public cancelUpload(uploadId: string): void {
|
||||
const controller = this.uploadQueue.get(uploadId)
|
||||
if (controller) {
|
||||
controller.abort()
|
||||
this.uploadQueue.delete(uploadId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有上传
|
||||
*/
|
||||
public cancelAllUploads(): void {
|
||||
this.uploadQueue.forEach((controller) => {
|
||||
controller.abort()
|
||||
})
|
||||
this.uploadQueue.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上传进度
|
||||
*/
|
||||
public getUploadProgress(uploadId: string): number {
|
||||
// 这里可以实现获取具体上传进度的逻辑
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// 创建默认实例
|
||||
export const fileUploader = new FileUploader()
|
||||
417
src/api/examples/usage.ts
Normal file
@ -0,0 +1,417 @@
|
||||
/**
|
||||
* API使用示例
|
||||
* 展示如何使用重构后的API架构
|
||||
*/
|
||||
|
||||
import {
|
||||
editorApi,
|
||||
resourceApi,
|
||||
fileUploader,
|
||||
fileDownloader,
|
||||
ApiManager,
|
||||
type ProjectData,
|
||||
type ResourceData
|
||||
} from '../index'
|
||||
|
||||
/**
|
||||
* 编辑器API使用示例
|
||||
*/
|
||||
export class EditorApiExamples {
|
||||
/**
|
||||
* 创建订单示例
|
||||
*/
|
||||
static async createOrderExample() {
|
||||
try {
|
||||
const response = await editorApi.createOrder({
|
||||
projectId: 'project-123',
|
||||
type: 'render',
|
||||
options: {
|
||||
quality: 'high',
|
||||
format: 'png'
|
||||
}
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
console.log('订单创建成功:', response.data)
|
||||
return response.data
|
||||
} else {
|
||||
console.error('订单创建失败:', response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建订单时发生错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存项目示例
|
||||
*/
|
||||
static async saveProjectExample() {
|
||||
try {
|
||||
const projectData: Partial<ProjectData> = {
|
||||
name: '我的3D项目',
|
||||
description: '这是一个示例项目',
|
||||
sceneData: {
|
||||
objects: [],
|
||||
lights: [],
|
||||
camera: {}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await editorApi.saveProject(projectData)
|
||||
|
||||
if (response.success) {
|
||||
console.log('项目保存成功:', response.data)
|
||||
return response.data
|
||||
} else {
|
||||
console.error('项目保存失败:', response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存项目时发生错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传缩略图示例
|
||||
*/
|
||||
static async uploadThumbnailExample(file: File, projectId: string) {
|
||||
try {
|
||||
const response = await editorApi.uploadThumbnail(file, projectId)
|
||||
|
||||
if (response.success) {
|
||||
console.log('缩略图上传成功:', response.data)
|
||||
return response.data
|
||||
} else {
|
||||
console.error('缩略图上传失败:', response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传缩略图时发生错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出项目示例
|
||||
*/
|
||||
static async exportProjectExample(projectId: string) {
|
||||
try {
|
||||
await editorApi.exportProject(projectId, 'glb')
|
||||
console.log('项目导出完成')
|
||||
} catch (error) {
|
||||
console.error('导出项目时发生错误:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源API使用示例
|
||||
*/
|
||||
export class ResourceApiExamples {
|
||||
/**
|
||||
* 上传资源示例
|
||||
*/
|
||||
static async uploadResourceExample(file: File) {
|
||||
try {
|
||||
const response = await resourceApi.uploadResource(file, {
|
||||
type: 'model',
|
||||
tags: ['建筑', '现代'],
|
||||
description: '现代建筑模型'
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
console.log('资源上传成功:', response.data)
|
||||
return response.data
|
||||
} else {
|
||||
console.error('资源上传失败:', response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传资源时发生错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上传资源示例
|
||||
*/
|
||||
static async uploadMultipleResourcesExample(files: File[]) {
|
||||
try {
|
||||
const response = await resourceApi.uploadMultipleResources(files, {
|
||||
type: 'texture',
|
||||
tags: ['材质', '金属']
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
console.log('批量上传成功:', response.data)
|
||||
return response.data
|
||||
} else {
|
||||
console.error('批量上传失败:', response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量上传时发生错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索资源示例
|
||||
*/
|
||||
static async searchResourcesExample() {
|
||||
try {
|
||||
const response = await resourceApi.searchResources({
|
||||
keyword: '建筑',
|
||||
type: 'model',
|
||||
tags: ['现代'],
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
console.log('搜索结果:', response.data)
|
||||
return response.data
|
||||
} else {
|
||||
console.error('搜索失败:', response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索资源时发生错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载资源示例
|
||||
*/
|
||||
static async downloadResourceExample(resource: ResourceData) {
|
||||
try {
|
||||
await resourceApi.downloadResource(resource)
|
||||
console.log('资源下载完成')
|
||||
} catch (error) {
|
||||
console.error('下载资源时发生错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载资源示例
|
||||
*/
|
||||
static async downloadMultipleResourcesExample(resources: ResourceData[]) {
|
||||
try {
|
||||
await resourceApi.downloadMultipleResources(resources)
|
||||
console.log('批量下载完成')
|
||||
} catch (error) {
|
||||
console.error('批量下载时发生错误:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传使用示例
|
||||
*/
|
||||
export class FileUploadExamples {
|
||||
/**
|
||||
* 普通文件上传示例
|
||||
*/
|
||||
static async uploadFileExample(file: File) {
|
||||
try {
|
||||
const response = await fileUploader.uploadFile({
|
||||
file,
|
||||
onProgress: (progress) => {
|
||||
console.log(`上传进度: ${progress}%`)
|
||||
}
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
console.log('文件上传成功:', response.data)
|
||||
return response.data
|
||||
} else {
|
||||
console.error('文件上传失败:', response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传文件时发生错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片上传示例(带压缩)
|
||||
*/
|
||||
static async uploadImageExample(file: File) {
|
||||
try {
|
||||
const response = await fileUploader.uploadImage(file, {
|
||||
compress: true,
|
||||
quality: 0.8,
|
||||
maxWidth: 1920,
|
||||
maxHeight: 1080,
|
||||
onProgress: (progress) => {
|
||||
console.log(`图片上传进度: ${progress}%`)
|
||||
}
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
console.log('图片上传成功:', response.data)
|
||||
return response.data
|
||||
} else {
|
||||
console.error('图片上传失败:', response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传图片时发生错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 大文件分片上传示例
|
||||
*/
|
||||
static async uploadLargeFileExample(file: File) {
|
||||
try {
|
||||
const response = await fileUploader.uploadLargeFile(file, {
|
||||
chunkSize: 2 * 1024 * 1024, // 2MB per chunk
|
||||
onProgress: (progress) => {
|
||||
console.log(`大文件上传进度: ${progress}%`)
|
||||
},
|
||||
onChunkProgress: (chunkIndex, chunkProgress) => {
|
||||
console.log(`分片 ${chunkIndex} 上传进度: ${chunkProgress}%`)
|
||||
}
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
console.log('大文件上传成功:', response.data)
|
||||
return response.data
|
||||
} else {
|
||||
console.error('大文件上传失败:', response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传大文件时发生错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量文件上传示例
|
||||
*/
|
||||
static async uploadBatchExample(files: File[]) {
|
||||
try {
|
||||
const result = await fileUploader.uploadBatch(files, {
|
||||
concurrent: 3,
|
||||
onProgress: (progress) => {
|
||||
console.log(`批量上传总进度: ${progress}%`)
|
||||
},
|
||||
onFileProgress: (file, progress) => {
|
||||
console.log(`文件 ${file.name} 上传进度: ${progress}%`)
|
||||
},
|
||||
onFileComplete: (file, result) => {
|
||||
if (result instanceof Error) {
|
||||
console.error(`文件 ${file.name} 上传失败:`, result)
|
||||
} else {
|
||||
console.log(`文件 ${file.name} 上传成功`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.log('批量上传结果:', result)
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('批量上传时发生错误:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件下载使用示例
|
||||
*/
|
||||
export class FileDownloadExamples {
|
||||
/**
|
||||
* 普通文件下载示例
|
||||
*/
|
||||
static async downloadFileExample(url: string, filename?: string) {
|
||||
try {
|
||||
await fileDownloader.downloadAndSave({
|
||||
url,
|
||||
filename,
|
||||
onProgress: (progress) => {
|
||||
console.log(`下载进度: ${progress}%`)
|
||||
}
|
||||
})
|
||||
|
||||
console.log('文件下载完成')
|
||||
} catch (error) {
|
||||
console.error('下载文件时发生错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载为Base64示例
|
||||
*/
|
||||
static async downloadAsBase64Example(url: string) {
|
||||
try {
|
||||
const base64 = await fileDownloader.downloadAsBase64({ url })
|
||||
console.log('文件下载为Base64完成:', base64.substring(0, 100) + '...')
|
||||
return base64
|
||||
} catch (error) {
|
||||
console.error('下载为Base64时发生错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载示例
|
||||
*/
|
||||
static async downloadBatchExample(urls: string[]) {
|
||||
try {
|
||||
const result = await fileDownloader.downloadBatch(urls, {
|
||||
concurrent: 3,
|
||||
onProgress: (progress) => {
|
||||
console.log(`批量下载总进度: ${progress}%`)
|
||||
},
|
||||
onFileProgress: (url, progress) => {
|
||||
console.log(`文件 ${url} 下载进度: ${progress}%`)
|
||||
},
|
||||
onFileComplete: (url, result) => {
|
||||
if (result instanceof Error) {
|
||||
console.error(`文件 ${url} 下载失败:`, result)
|
||||
} else {
|
||||
console.log(`文件 ${url} 下载成功`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.log('批量下载结果:', result)
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('批量下载时发生错误:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API管理器使用示例
|
||||
*/
|
||||
export class ApiManagerExamples {
|
||||
/**
|
||||
* 设置认证token示例
|
||||
*/
|
||||
static setAuthTokenExample(token: string) {
|
||||
ApiManager.setAuthToken(token)
|
||||
console.log('认证token已设置')
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查网络连接示例
|
||||
*/
|
||||
static async checkConnectionExample() {
|
||||
try {
|
||||
const isConnected = await ApiManager.checkConnection()
|
||||
console.log('网络连接状态:', isConnected ? '已连接' : '未连接')
|
||||
return isConnected
|
||||
} catch (error) {
|
||||
console.error('检查网络连接时发生错误:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取API配置示例
|
||||
*/
|
||||
static getConfigExample() {
|
||||
const config = ApiManager.getConfig()
|
||||
console.log('API配置:', config)
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有请求示例
|
||||
*/
|
||||
static cancelAllRequestsExample() {
|
||||
ApiManager.cancelAllRequests()
|
||||
console.log('所有请求已取消')
|
||||
}
|
||||
}
|
||||
116
src/api/index.ts
Normal file
@ -0,0 +1,116 @@
|
||||
// 核心模块
|
||||
export { httpClient, HttpClient } from './core/request'
|
||||
export { fileUploader, FileUploader } from './core/upload'
|
||||
export { fileDownloader, FileDownloader } from './core/download'
|
||||
|
||||
// 业务API模块
|
||||
export { editorApi, EditorApi } from './modules/editor'
|
||||
export { resourceApi, ResourceApi } from './modules/resource'
|
||||
|
||||
// 类型定义
|
||||
export type {
|
||||
ApiResponse,
|
||||
RequestConfig,
|
||||
UploadConfig,
|
||||
DownloadConfig,
|
||||
PaginationParams,
|
||||
PaginationResponse,
|
||||
ApiError
|
||||
} from './types'
|
||||
|
||||
// 配置
|
||||
export { API_CONFIG, API_ENDPOINTS, ENV_CONFIG } from './config'
|
||||
|
||||
// 工具函数
|
||||
export {
|
||||
delay,
|
||||
generateId,
|
||||
formatFileSize,
|
||||
getFileExtension,
|
||||
validateFileType,
|
||||
createApiError,
|
||||
isApiError,
|
||||
retry,
|
||||
downloadFile,
|
||||
readFileAsBase64,
|
||||
readFileAsArrayBuffer,
|
||||
compressImage
|
||||
} from './utils'
|
||||
|
||||
// 业务类型
|
||||
export type {
|
||||
ProjectData,
|
||||
OrderData
|
||||
} from './modules/editor'
|
||||
|
||||
export type {
|
||||
ResourceData,
|
||||
ResourceCategory,
|
||||
ResourceSearchParams
|
||||
} from './modules/resource'
|
||||
|
||||
// 上传下载相关类型
|
||||
export type {
|
||||
UploadResponse,
|
||||
BatchUploadResponse
|
||||
} from './core/upload'
|
||||
|
||||
export type {
|
||||
DownloadResponse,
|
||||
BatchDownloadResponse
|
||||
} from './core/download'
|
||||
|
||||
// 导入实例用于ApiManager
|
||||
import { httpClient } from './core/request'
|
||||
import { fileUploader } from './core/upload'
|
||||
import { fileDownloader } from './core/download'
|
||||
import { API_CONFIG, ENV_CONFIG } from './config'
|
||||
|
||||
/**
|
||||
* API管理器类
|
||||
* 提供统一的API管理和配置
|
||||
*/
|
||||
export class ApiManager {
|
||||
/**
|
||||
* 设置认证token
|
||||
*/
|
||||
public static setAuthToken(token: string): void {
|
||||
httpClient.setAuthToken(token)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有请求
|
||||
*/
|
||||
public static cancelAllRequests(): void {
|
||||
httpClient.cancelAllRequests()
|
||||
fileUploader.cancelAllUploads()
|
||||
fileDownloader.cancelAllDownloads()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取API配置
|
||||
*/
|
||||
public static getConfig() {
|
||||
return {
|
||||
baseUrl: API_CONFIG.BASE_URL,
|
||||
timeout: API_CONFIG.TIMEOUT,
|
||||
retryCount: API_CONFIG.RETRY_COUNT,
|
||||
env: ENV_CONFIG
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查网络连接
|
||||
*/
|
||||
public static async checkConnection(): Promise<boolean> {
|
||||
try {
|
||||
const response = await httpClient.get('/health')
|
||||
return response.success
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 默认导出API管理器
|
||||
export default ApiManager
|
||||
143
src/api/modules/editor.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import { httpClient } from '../core/request'
|
||||
import { fileUploader } from '../core/upload'
|
||||
import { fileDownloader } from '../core/download'
|
||||
import { ApiResponse, PaginationParams, PaginationResponse } from '../types'
|
||||
import { API_ENDPOINTS } from '../config'
|
||||
|
||||
/**
|
||||
* 项目数据类型
|
||||
*/
|
||||
export interface ProjectData {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
thumbnail?: string
|
||||
sceneData: any
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
userId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单数据类型
|
||||
*/
|
||||
export interface OrderData {
|
||||
id: string
|
||||
projectId: string
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed'
|
||||
result?: any
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器API类
|
||||
*/
|
||||
export class EditorApi {
|
||||
/**
|
||||
* 创建订单
|
||||
*/
|
||||
public async createOrder(params: any): Promise<ApiResponse<OrderData>> {
|
||||
return httpClient.post<OrderData>(API_ENDPOINTS.EDITOR.CREATE_ORDER, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存项目
|
||||
*/
|
||||
public async saveProject(projectData: Partial<ProjectData>): Promise<ApiResponse<ProjectData>> {
|
||||
return httpClient.post<ProjectData>(API_ENDPOINTS.EDITOR.SAVE_PROJECT, projectData)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载项目
|
||||
*/
|
||||
public async loadProject(projectId: string): Promise<ApiResponse<ProjectData>> {
|
||||
return httpClient.get<ProjectData>(`${API_ENDPOINTS.EDITOR.LOAD_PROJECT}/${projectId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除项目
|
||||
*/
|
||||
public async deleteProject(projectId: string): Promise<ApiResponse<void>> {
|
||||
return httpClient.delete<void>(`${API_ENDPOINTS.EDITOR.DELETE_PROJECT}/${projectId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目列表
|
||||
*/
|
||||
public async getProjectList(params: PaginationParams): Promise<ApiResponse<PaginationResponse<ProjectData>>> {
|
||||
return httpClient.get<PaginationResponse<ProjectData>>(API_ENDPOINTS.EDITOR.LOAD_PROJECT, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传项目缩略图
|
||||
*/
|
||||
public async uploadThumbnail(file: File, projectId: string): Promise<ApiResponse<{ url: string }>> {
|
||||
return fileUploader.uploadImage(file, {
|
||||
compress: true,
|
||||
quality: 0.8,
|
||||
maxWidth: 400,
|
||||
maxHeight: 300,
|
||||
onProgress: (progress) => {
|
||||
console.log(`缩略图上传进度: ${progress}%`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出项目
|
||||
*/
|
||||
public async exportProject(projectId: string, format: 'json' | 'glb' | 'fbx' = 'json'): Promise<void> {
|
||||
await fileDownloader.downloadAndSave({
|
||||
url: `/editor/project/export/${projectId}`,
|
||||
filename: `project_${projectId}.${format}`,
|
||||
params: { format }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入项目
|
||||
*/
|
||||
public async importProject(file: File): Promise<ApiResponse<ProjectData>> {
|
||||
const uploadResponse = await fileUploader.uploadFile({
|
||||
file,
|
||||
fieldName: 'projectFile',
|
||||
data: {
|
||||
action: 'import'
|
||||
}
|
||||
})
|
||||
|
||||
if (!uploadResponse.success) {
|
||||
throw new Error(uploadResponse.message)
|
||||
}
|
||||
|
||||
// 上传成功后,调用导入API
|
||||
return httpClient.post<ProjectData>('/editor/project/import', {
|
||||
fileUrl: uploadResponse.data.url
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导出项目
|
||||
*/
|
||||
public async exportMultipleProjects(projectIds: string[], format: 'json' | 'glb' | 'fbx' = 'json'): Promise<void> {
|
||||
const downloadUrls = projectIds.map(id => `/editor/project/export/${id}?format=${format}`)
|
||||
|
||||
await fileDownloader.downloadBatch(downloadUrls, {
|
||||
concurrent: 2,
|
||||
onProgress: (progress) => {
|
||||
console.log(`批量导出进度: ${progress}%`)
|
||||
},
|
||||
onFileComplete: (url, result) => {
|
||||
if (result instanceof Error) {
|
||||
console.error(`导出失败: ${url}`, result)
|
||||
} else {
|
||||
console.log(`导出完成: ${url}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 创建默认实例
|
||||
export const editorApi = new EditorApi()
|
||||
281
src/api/modules/resource.ts
Normal file
@ -0,0 +1,281 @@
|
||||
import { httpClient } from '../core/request'
|
||||
import { fileUploader } from '../core/upload'
|
||||
import { fileDownloader } from '../core/download'
|
||||
import { ApiResponse, PaginationParams, PaginationResponse } from '../types'
|
||||
import { API_ENDPOINTS } from '../config'
|
||||
|
||||
/**
|
||||
* 资源数据类型
|
||||
*/
|
||||
export interface ResourceData {
|
||||
id: string
|
||||
name: string
|
||||
type: 'model' | 'texture' | 'material' | 'audio' | 'video' | 'other'
|
||||
url: string
|
||||
thumbnailUrl?: string
|
||||
size: number
|
||||
format: string
|
||||
tags: string[]
|
||||
description?: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
userId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源分类类型
|
||||
*/
|
||||
export interface ResourceCategory {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
parentId?: string
|
||||
children?: ResourceCategory[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源搜索参数
|
||||
*/
|
||||
export interface ResourceSearchParams extends PaginationParams {
|
||||
keyword?: string
|
||||
type?: ResourceData['type']
|
||||
tags?: string[]
|
||||
categoryId?: string
|
||||
format?: string
|
||||
sizeMin?: number
|
||||
sizeMax?: number
|
||||
dateFrom?: string
|
||||
dateTo?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源API类
|
||||
*/
|
||||
export class ResourceApi {
|
||||
/**
|
||||
* 获取资源列表
|
||||
*/
|
||||
public async getResourceList(params: ResourceSearchParams): Promise<ApiResponse<PaginationResponse<ResourceData>>> {
|
||||
return httpClient.get<PaginationResponse<ResourceData>>(API_ENDPOINTS.RESOURCE.LIST, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源详情
|
||||
*/
|
||||
public async getResourceDetail(resourceId: string): Promise<ApiResponse<ResourceData>> {
|
||||
return httpClient.get<ResourceData>(`${API_ENDPOINTS.RESOURCE.LIST}/${resourceId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传资源
|
||||
*/
|
||||
public async uploadResource(
|
||||
file: File,
|
||||
metadata: {
|
||||
name?: string
|
||||
type: ResourceData['type']
|
||||
tags?: string[]
|
||||
description?: string
|
||||
categoryId?: string
|
||||
}
|
||||
): Promise<ApiResponse<ResourceData>> {
|
||||
// 根据文件类型选择上传方式
|
||||
let uploadResponse
|
||||
|
||||
if (metadata.type === 'texture' && file.type.startsWith('image/')) {
|
||||
// 图片资源,使用图片上传(带压缩)
|
||||
uploadResponse = await fileUploader.uploadImage(file, {
|
||||
compress: true,
|
||||
quality: 0.9,
|
||||
maxWidth: 2048,
|
||||
maxHeight: 2048,
|
||||
onProgress: (progress) => {
|
||||
console.log(`资源上传进度: ${progress}%`)
|
||||
}
|
||||
})
|
||||
} else if (file.size > 50 * 1024 * 1024) {
|
||||
// 大文件,使用分片上传
|
||||
uploadResponse = await fileUploader.uploadLargeFile(file, {
|
||||
onProgress: (progress) => {
|
||||
console.log(`大文件上传进度: ${progress}%`)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 普通文件上传
|
||||
uploadResponse = await fileUploader.uploadFile({
|
||||
file,
|
||||
data: metadata,
|
||||
onProgress: (progress) => {
|
||||
console.log(`文件上传进度: ${progress}%`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!uploadResponse.success) {
|
||||
throw new Error(uploadResponse.message)
|
||||
}
|
||||
|
||||
// 创建资源记录
|
||||
return httpClient.post<ResourceData>(API_ENDPOINTS.RESOURCE.UPLOAD, {
|
||||
...metadata,
|
||||
name: metadata.name || file.name,
|
||||
url: uploadResponse.data.url,
|
||||
size: file.size,
|
||||
format: file.name.split('.').pop()?.toLowerCase() || 'unknown'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上传资源
|
||||
*/
|
||||
public async uploadMultipleResources(
|
||||
files: File[],
|
||||
metadata: {
|
||||
type: ResourceData['type']
|
||||
tags?: string[]
|
||||
categoryId?: string
|
||||
}
|
||||
): Promise<ApiResponse<ResourceData[]>> {
|
||||
const results = await fileUploader.uploadBatch(files, {
|
||||
concurrent: 3,
|
||||
onProgress: (progress) => {
|
||||
console.log(`批量上传进度: ${progress}%`)
|
||||
},
|
||||
onFileComplete: (file, result) => {
|
||||
if (result instanceof Error) {
|
||||
console.error(`文件上传失败: ${file.name}`, result)
|
||||
} else {
|
||||
console.log(`文件上传完成: ${file.name}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 创建资源记录
|
||||
const resourcePromises = results.success.map(uploadResult =>
|
||||
httpClient.post<ResourceData>(API_ENDPOINTS.RESOURCE.UPLOAD, {
|
||||
...metadata,
|
||||
name: uploadResult.filename,
|
||||
url: uploadResult.url,
|
||||
size: uploadResult.size,
|
||||
format: uploadResult.filename.split('.').pop()?.toLowerCase() || 'unknown'
|
||||
})
|
||||
)
|
||||
|
||||
const resourceResults = await Promise.all(resourcePromises)
|
||||
const successResources = resourceResults
|
||||
.filter(result => result.success)
|
||||
.map(result => result.data)
|
||||
|
||||
return {
|
||||
code: 0,
|
||||
message: 'Success',
|
||||
data: successResources,
|
||||
success: true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除资源
|
||||
*/
|
||||
public async deleteResource(resourceId: string): Promise<ApiResponse<void>> {
|
||||
return httpClient.delete<void>(`${API_ENDPOINTS.RESOURCE.DELETE}/${resourceId}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除资源
|
||||
*/
|
||||
public async deleteMultipleResources(resourceIds: string[]): Promise<ApiResponse<void>> {
|
||||
return httpClient.post<void>('/resource/batch-delete', { resourceIds })
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新资源信息
|
||||
*/
|
||||
public async updateResource(
|
||||
resourceId: string,
|
||||
data: Partial<Pick<ResourceData, 'name' | 'tags' | 'description'>>
|
||||
): Promise<ApiResponse<ResourceData>> {
|
||||
return httpClient.put<ResourceData>(`/resource/${resourceId}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载资源
|
||||
*/
|
||||
public async downloadResource(resource: ResourceData): Promise<void> {
|
||||
await fileDownloader.downloadAndSave({
|
||||
url: resource.url,
|
||||
filename: resource.name
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载资源
|
||||
*/
|
||||
public async downloadMultipleResources(resources: ResourceData[]): Promise<void> {
|
||||
const downloadFiles = resources.map(resource => ({
|
||||
url: resource.url,
|
||||
filename: resource.name
|
||||
}))
|
||||
|
||||
await fileDownloader.downloadAsZip(downloadFiles, 'resources.zip')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源分类
|
||||
*/
|
||||
public async getResourceCategories(): Promise<ApiResponse<ResourceCategory[]>> {
|
||||
return httpClient.get<ResourceCategory[]>('/resource/categories')
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建资源分类
|
||||
*/
|
||||
public async createResourceCategory(
|
||||
data: Pick<ResourceCategory, 'name' | 'description' | 'parentId'>
|
||||
): Promise<ApiResponse<ResourceCategory>> {
|
||||
return httpClient.post<ResourceCategory>('/resource/categories', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索资源
|
||||
*/
|
||||
public async searchResources(params: ResourceSearchParams): Promise<ApiResponse<PaginationResponse<ResourceData>>> {
|
||||
return httpClient.get<PaginationResponse<ResourceData>>('/resource/search', params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门标签
|
||||
*/
|
||||
public async getPopularTags(limit: number = 20): Promise<ApiResponse<string[]>> {
|
||||
return httpClient.get<string[]>('/resource/tags/popular', { limit })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源统计信息
|
||||
*/
|
||||
public async getResourceStats(): Promise<ApiResponse<{
|
||||
total: number
|
||||
byType: Record<ResourceData['type'], number>
|
||||
totalSize: number
|
||||
recentUploads: number
|
||||
}>> {
|
||||
return httpClient.get('/resource/stats')
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览资源(获取预览URL)
|
||||
*/
|
||||
public async getResourcePreview(resourceId: string): Promise<ApiResponse<{ previewUrl: string }>> {
|
||||
return httpClient.get<{ previewUrl: string }>(`/resource/${resourceId}/preview`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成资源缩略图
|
||||
*/
|
||||
public async generateThumbnail(resourceId: string): Promise<ApiResponse<{ thumbnailUrl: string }>> {
|
||||
return httpClient.post<{ thumbnailUrl: string }>(`/resource/${resourceId}/thumbnail`)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建默认实例
|
||||
export const resourceApi = new ResourceApi()
|
||||
58
src/api/types/index.ts
Normal file
@ -0,0 +1,58 @@
|
||||
// 通用响应类型
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
success: boolean
|
||||
}
|
||||
|
||||
// 请求配置类型
|
||||
export interface RequestConfig {
|
||||
url: string
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'
|
||||
params?: Record<string, any>
|
||||
data?: any
|
||||
headers?: Record<string, string>
|
||||
timeout?: number
|
||||
responseType?: 'json' | 'blob' | 'text' | 'arraybuffer'
|
||||
}
|
||||
|
||||
// 文件上传配置
|
||||
export interface UploadConfig {
|
||||
file: File | Blob
|
||||
filename?: string
|
||||
fieldName?: string
|
||||
data?: Record<string, any>
|
||||
onProgress?: (progress: number) => void
|
||||
}
|
||||
|
||||
// 文件下载配置
|
||||
export interface DownloadConfig {
|
||||
url: string
|
||||
filename?: string
|
||||
params?: Record<string, any>
|
||||
onProgress?: (progress: number) => void
|
||||
}
|
||||
|
||||
// 分页请求参数
|
||||
export interface PaginationParams {
|
||||
page: number
|
||||
pageSize: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
// 分页响应数据
|
||||
export interface PaginationResponse<T> {
|
||||
list: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
// 错误类型
|
||||
export interface ApiError {
|
||||
code: number
|
||||
message: string
|
||||
details?: any
|
||||
}
|
||||
184
src/api/utils/index.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import { ApiError } from '../types'
|
||||
import { API_CONFIG } from '../config'
|
||||
|
||||
/**
|
||||
* 延迟函数
|
||||
*/
|
||||
export const delay = (ms: number): Promise<void> => {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
*/
|
||||
export const generateId = (): string => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件大小
|
||||
*/
|
||||
export const formatFileSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
|
||||
const k = 1024
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
*/
|
||||
export const getFileExtension = (filename: string): string => {
|
||||
return filename.slice((filename.lastIndexOf('.') - 1 >>> 0) + 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文件类型
|
||||
*/
|
||||
export const validateFileType = (file: File, allowedTypes: string[]): boolean => {
|
||||
const fileType = file.type
|
||||
const fileExtension = getFileExtension(file.name).toLowerCase()
|
||||
|
||||
return allowedTypes.some(type => {
|
||||
if (type.includes('/')) {
|
||||
return fileType === type
|
||||
}
|
||||
return fileExtension === type.toLowerCase()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建API错误对象
|
||||
*/
|
||||
export const createApiError = (
|
||||
code: number,
|
||||
message: string,
|
||||
details?: any
|
||||
): ApiError => {
|
||||
return {
|
||||
code,
|
||||
message,
|
||||
details
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为API错误
|
||||
*/
|
||||
export const isApiError = (error: any): error is ApiError => {
|
||||
return error && typeof error.code === 'number' && typeof error.message === 'string'
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试函数
|
||||
*/
|
||||
export const retry = async <T>(
|
||||
fn: () => Promise<T>,
|
||||
retries: number = API_CONFIG.RETRY_COUNT,
|
||||
delayMs: number = API_CONFIG.RETRY_DELAY
|
||||
): Promise<T> => {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (error) {
|
||||
if (retries > 0) {
|
||||
await delay(delayMs)
|
||||
return retry(fn, retries - 1, delayMs * 2) // 指数退避
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
export const downloadFile = (blob: Blob, filename: string): void => {
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取文件为Base64
|
||||
*/
|
||||
export const readFileAsBase64 = (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const result = reader.result as string
|
||||
resolve(result.split(',')[1]) // 移除data:xxx;base64,前缀
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取文件为ArrayBuffer
|
||||
*/
|
||||
export const readFileAsArrayBuffer = (file: File): Promise<ArrayBuffer> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(reader.result as ArrayBuffer)
|
||||
reader.onerror = reject
|
||||
reader.readAsArrayBuffer(file)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩图片
|
||||
*/
|
||||
export const compressImage = (
|
||||
file: File,
|
||||
quality: number = 0.8,
|
||||
maxWidth: number = 1920,
|
||||
maxHeight: number = 1080
|
||||
): Promise<Blob> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
const img = new Image()
|
||||
|
||||
img.onload = () => {
|
||||
// 计算压缩后的尺寸
|
||||
let { width, height } = img
|
||||
|
||||
if (width > maxWidth) {
|
||||
height = (height * maxWidth) / width
|
||||
width = maxWidth
|
||||
}
|
||||
|
||||
if (height > maxHeight) {
|
||||
width = (width * maxHeight) / height
|
||||
height = maxHeight
|
||||
}
|
||||
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
|
||||
// 绘制并压缩
|
||||
ctx?.drawImage(img, 0, 0, width, height)
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob) {
|
||||
resolve(blob)
|
||||
} else {
|
||||
reject(new Error('图片压缩失败'))
|
||||
}
|
||||
},
|
||||
file.type,
|
||||
quality
|
||||
)
|
||||
}
|
||||
|
||||
img.onerror = reject
|
||||
img.src = URL.createObjectURL(file)
|
||||
})
|
||||
}
|
||||
BIN
src/assets/font/QingNiaoHuaGuangJianMeiHei-2.ttf
Normal file
BIN
src/assets/logo.jpg
Normal file
|
After Width: | Height: | Size: 586 KiB |
BIN
src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
46
src/assets/style/components.css
Normal file
@ -0,0 +1,46 @@
|
||||
.inspectortotip {
|
||||
margin-top: 5px;
|
||||
color: rgba(250, 92, 0, 0.712);
|
||||
width: 200px;
|
||||
margin-left: 20px;
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.slider-them {
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
cursor: ew-resize;
|
||||
|
||||
width: 20px;
|
||||
height: 40px;
|
||||
background-color: #006eff;
|
||||
opacity: 0.7;
|
||||
border-radius: 1rem;
|
||||
|
||||
top: calc(50% - 20px);
|
||||
left: calc(50% - 10px);
|
||||
|
||||
}
|
||||
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
cursor: ew-resize;
|
||||
width: 40px;
|
||||
height: 60px;
|
||||
background-color: #7777773a;
|
||||
opacity: 0.7;
|
||||
border-radius: 1rem;
|
||||
top: calc(50% - 20px);
|
||||
left: calc(50% - 10px);
|
||||
|
||||
}
|
||||
539
src/assets/style/font_oy0t1irww4k/demo.css
Normal file
@ -0,0 +1,539 @@
|
||||
/* Logo 字体 */
|
||||
@font-face {
|
||||
font-family: "iconfont logo";
|
||||
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
|
||||
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
|
||||
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-family: "iconfont logo";
|
||||
font-size: 160px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* tabs */
|
||||
.nav-tabs {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-more {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#tabs {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
#tabs li {
|
||||
cursor: pointer;
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
border-bottom: 2px solid transparent;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-bottom: -1px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
#tabs .active {
|
||||
border-bottom-color: #f00;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.tab-container .content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 页面布局 */
|
||||
.main {
|
||||
padding: 30px 100px;
|
||||
width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.main .logo {
|
||||
color: #333;
|
||||
text-align: left;
|
||||
margin-bottom: 30px;
|
||||
line-height: 1;
|
||||
height: 110px;
|
||||
margin-top: -50px;
|
||||
overflow: hidden;
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
.main .logo a {
|
||||
font-size: 160px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.helps {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.helps pre {
|
||||
padding: 20px;
|
||||
margin: 10px 0;
|
||||
border: solid 1px #e7e1cd;
|
||||
background-color: #fffdef;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.icon_lists {
|
||||
width: 100% !important;
|
||||
overflow: hidden;
|
||||
*zoom: 1;
|
||||
}
|
||||
|
||||
.icon_lists li {
|
||||
width: 100px;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 20px;
|
||||
text-align: center;
|
||||
list-style: none !important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.icon_lists li .code-name {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.icon_lists .icon {
|
||||
display: block;
|
||||
height: 100px;
|
||||
line-height: 100px;
|
||||
font-size: 42px;
|
||||
margin: 10px auto;
|
||||
color: #333;
|
||||
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
|
||||
-moz-transition: font-size 0.25s linear, width 0.25s linear;
|
||||
transition: font-size 0.25s linear, width 0.25s linear;
|
||||
}
|
||||
|
||||
.icon_lists .icon:hover {
|
||||
font-size: 100px;
|
||||
}
|
||||
|
||||
.icon_lists .svg-icon {
|
||||
/* 通过设置 font-size 来改变图标大小 */
|
||||
width: 1em;
|
||||
/* 图标和文字相邻时,垂直对齐 */
|
||||
vertical-align: -0.15em;
|
||||
/* 通过设置 color 来改变 SVG 的颜色/fill */
|
||||
fill: currentColor;
|
||||
/* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
|
||||
normalize.css 中也包含这行 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon_lists li .name,
|
||||
.icon_lists li .code-name {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* markdown 样式 */
|
||||
.markdown {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.markdown img {
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
color: #404040;
|
||||
font-weight: 500;
|
||||
line-height: 40px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.markdown h2,
|
||||
.markdown h3,
|
||||
.markdown h4,
|
||||
.markdown h5,
|
||||
.markdown h6 {
|
||||
color: #404040;
|
||||
margin: 1.6em 0 0.6em 0;
|
||||
font-weight: 500;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.markdown h2 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.markdown h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.markdown h4 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.markdown h5 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown h6 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.markdown hr {
|
||||
height: 1px;
|
||||
border: 0;
|
||||
background: #e9e9e9;
|
||||
margin: 16px 0;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.markdown p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown>p,
|
||||
.markdown>blockquote,
|
||||
.markdown>.highlight,
|
||||
.markdown>ol,
|
||||
.markdown>ul {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.markdown ul>li {
|
||||
list-style: circle;
|
||||
}
|
||||
|
||||
.markdown>ul li,
|
||||
.markdown blockquote ul>li {
|
||||
margin-left: 20px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.markdown>ul li p,
|
||||
.markdown>ol li p {
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
||||
.markdown ol>li {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
.markdown>ol li,
|
||||
.markdown blockquote ol>li {
|
||||
margin-left: 20px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.markdown code {
|
||||
margin: 0 3px;
|
||||
padding: 0 5px;
|
||||
background: #eee;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.markdown strong,
|
||||
.markdown b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown>table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0px;
|
||||
empty-cells: show;
|
||||
border: 1px solid #e9e9e9;
|
||||
width: 95%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.markdown>table th {
|
||||
white-space: nowrap;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown>table th,
|
||||
.markdown>table td {
|
||||
border: 1px solid #e9e9e9;
|
||||
padding: 8px 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown>table th {
|
||||
background: #F7F7F7;
|
||||
}
|
||||
|
||||
.markdown blockquote {
|
||||
font-size: 90%;
|
||||
color: #999;
|
||||
border-left: 4px solid #e9e9e9;
|
||||
padding-left: 0.8em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.markdown blockquote p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.markdown .anchor {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.markdown .waiting {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.markdown h1:hover .anchor,
|
||||
.markdown h2:hover .anchor,
|
||||
.markdown h3:hover .anchor,
|
||||
.markdown h4:hover .anchor,
|
||||
.markdown h5:hover .anchor,
|
||||
.markdown h6:hover .anchor {
|
||||
opacity: 1;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.markdown>br,
|
||||
.markdown>p>br {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
background: white;
|
||||
padding: 0.5em;
|
||||
color: #333333;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-meta {
|
||||
color: #969896;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-strong,
|
||||
.hljs-emphasis,
|
||||
.hljs-quote {
|
||||
color: #df5000;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-type {
|
||||
color: #a71d5d;
|
||||
}
|
||||
|
||||
.hljs-literal,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-attribute {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-section,
|
||||
.hljs-name {
|
||||
color: #63a35c;
|
||||
}
|
||||
|
||||
.hljs-tag {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-attr,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #795da3;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #55a532;
|
||||
background-color: #eaffea;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #bd2c00;
|
||||
background-color: #ffecec;
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 代码高亮 */
|
||||
/* PrismJS 1.15.0
|
||||
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
|
||||
/**
|
||||
* prism.js default theme for JavaScript, CSS and HTML
|
||||
* Based on dabblet (http://dabblet.com)
|
||||
* @author Lea Verou
|
||||
*/
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: black;
|
||||
background: none;
|
||||
text-shadow: 0 1px white;
|
||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: 1.5;
|
||||
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::-moz-selection,
|
||||
pre[class*="language-"] ::-moz-selection,
|
||||
code[class*="language-"]::-moz-selection,
|
||||
code[class*="language-"] ::-moz-selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
pre[class*="language-"]::selection,
|
||||
pre[class*="language-"] ::selection,
|
||||
code[class*="language-"]::selection,
|
||||
code[class*="language-"] ::selection {
|
||||
text-shadow: none;
|
||||
background: #b3d4fc;
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:not(pre)>code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
background: #f5f2f0;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre)>code[class*="language-"] {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: slategray;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag,
|
||||
.token.boolean,
|
||||
.token.number,
|
||||
.token.constant,
|
||||
.token.symbol,
|
||||
.token.deleted {
|
||||
color: #905;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string,
|
||||
.token.char,
|
||||
.token.builtin,
|
||||
.token.inserted {
|
||||
color: #690;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #9a6e3a;
|
||||
background: hsla(0, 0%, 100%, .5);
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value,
|
||||
.token.keyword {
|
||||
color: #07a;
|
||||
}
|
||||
|
||||
.token.function,
|
||||
.token.class-name {
|
||||
color: #DD4A68;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important,
|
||||
.token.variable {
|
||||
color: #e90;
|
||||
}
|
||||
|
||||
.token.important,
|
||||
.token.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
391
src/assets/style/font_oy0t1irww4k/demo_index.html
Normal file
@ -0,0 +1,391 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>iconfont Demo</title>
|
||||
<link rel="shortcut icon" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg" type="image/x-icon"/>
|
||||
<link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg"/>
|
||||
<link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
|
||||
<link rel="stylesheet" href="demo.css">
|
||||
<link rel="stylesheet" href="iconfont.css">
|
||||
<script src="iconfont.js"></script>
|
||||
<!-- jQuery -->
|
||||
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
|
||||
<!-- 代码高亮 -->
|
||||
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
|
||||
<style>
|
||||
.main .logo {
|
||||
margin-top: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.main .logo a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main .logo .sub-title {
|
||||
margin-left: 0.5em;
|
||||
font-size: 22px;
|
||||
color: #fff;
|
||||
background: linear-gradient(-45deg, #3967FF, #B500FE);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main">
|
||||
<h1 class="logo"><a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">
|
||||
<img width="200" src="https://img.alicdn.com/imgextra/i3/O1CN01Mn65HV1FfSEzR6DKv_!!6000000000514-55-tps-228-59.svg">
|
||||
|
||||
</a></h1>
|
||||
<div class="nav-tabs">
|
||||
<ul id="tabs" class="dib-box">
|
||||
<li class="dib active"><span>Unicode</span></li>
|
||||
<li class="dib"><span>Font class</span></li>
|
||||
<li class="dib"><span>Symbol</span></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<div class="tab-container">
|
||||
<div class="content unicode" style="display: block;">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">全屏</div>
|
||||
<div class="code-name">&#xe660;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">帮助</div>
|
||||
<div class="code-name">&#xe620;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">帮助_o</div>
|
||||
<div class="code-name">&#xeb72;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">半透明(2)</div>
|
||||
<div class="code-name">&#xe60b;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">持续时间</div>
|
||||
<div class="code-name">&#xe71a;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">对比</div>
|
||||
<div class="code-name">&#xe60c;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">还原</div>
|
||||
<div class="code-name">&#xe604;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">vr</div>
|
||||
<div class="code-name">&#xe70a;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">持续时间</div>
|
||||
<div class="code-name">&#xe51b;</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="article markdown">
|
||||
<h2 id="unicode-">Unicode 引用</h2>
|
||||
<hr>
|
||||
|
||||
<p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
|
||||
<ul>
|
||||
<li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
|
||||
<li>默认情况下不支持多色,直接添加多色图标会自动去色。</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>注意:新版 iconfont 支持两种方式引用多色图标:SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)</p>
|
||||
</blockquote>
|
||||
<p>Unicode 使用步骤如下:</p>
|
||||
<h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
|
||||
<pre><code class="language-css"
|
||||
>@font-face {
|
||||
font-family: 'iconfont';
|
||||
src: url('iconfont.ttf?t=1733881413198') format('truetype');
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||
<pre><code class="language-css"
|
||||
>.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
|
||||
<pre>
|
||||
<code class="language-html"
|
||||
><span class="iconfont">&#x33;</span>
|
||||
</code></pre>
|
||||
<blockquote>
|
||||
<p>"iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content font-class">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-quanping"></span>
|
||||
<div class="name">
|
||||
全屏
|
||||
</div>
|
||||
<div class="code-name">.icon-quanping
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-bangzhu"></span>
|
||||
<div class="name">
|
||||
帮助
|
||||
</div>
|
||||
<div class="code-name">.icon-bangzhu
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-bangzhu_o"></span>
|
||||
<div class="name">
|
||||
帮助_o
|
||||
</div>
|
||||
<div class="code-name">.icon-bangzhu_o
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-bantouming"></span>
|
||||
<div class="name">
|
||||
半透明(2)
|
||||
</div>
|
||||
<div class="code-name">.icon-bantouming
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-chixushijian"></span>
|
||||
<div class="name">
|
||||
持续时间
|
||||
</div>
|
||||
<div class="code-name">.icon-chixushijian
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-duibi"></span>
|
||||
<div class="name">
|
||||
对比
|
||||
</div>
|
||||
<div class="code-name">.icon-duibi
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-huanyuan"></span>
|
||||
<div class="name">
|
||||
还原
|
||||
</div>
|
||||
<div class="code-name">.icon-huanyuan
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-vr"></span>
|
||||
<div class="name">
|
||||
vr
|
||||
</div>
|
||||
<div class="code-name">.icon-vr
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-chixushijian1"></span>
|
||||
<div class="name">
|
||||
持续时间
|
||||
</div>
|
||||
<div class="code-name">.icon-chixushijian1
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="article markdown">
|
||||
<h2 id="font-class-">font-class 引用</h2>
|
||||
<hr>
|
||||
|
||||
<p>font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。</p>
|
||||
<p>与 Unicode 使用方式相比,具有如下特点:</p>
|
||||
<ul>
|
||||
<li>相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。</li>
|
||||
<li>因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。</li>
|
||||
</ul>
|
||||
<p>使用步骤如下:</p>
|
||||
<h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
|
||||
<pre><code class="language-html"><link rel="stylesheet" href="./iconfont.css">
|
||||
</code></pre>
|
||||
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
|
||||
<pre><code class="language-html"><span class="iconfont icon-xxx"></span>
|
||||
</code></pre>
|
||||
<blockquote>
|
||||
<p>"
|
||||
iconfont" 是你项目下的 font-family。可以通过编辑项目查看,默认是 "iconfont"。</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content symbol">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-quanping"></use>
|
||||
</svg>
|
||||
<div class="name">全屏</div>
|
||||
<div class="code-name">#icon-quanping</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-bangzhu"></use>
|
||||
</svg>
|
||||
<div class="name">帮助</div>
|
||||
<div class="code-name">#icon-bangzhu</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-bangzhu_o"></use>
|
||||
</svg>
|
||||
<div class="name">帮助_o</div>
|
||||
<div class="code-name">#icon-bangzhu_o</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-bantouming"></use>
|
||||
</svg>
|
||||
<div class="name">半透明(2)</div>
|
||||
<div class="code-name">#icon-bantouming</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-chixushijian"></use>
|
||||
</svg>
|
||||
<div class="name">持续时间</div>
|
||||
<div class="code-name">#icon-chixushijian</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-duibi"></use>
|
||||
</svg>
|
||||
<div class="name">对比</div>
|
||||
<div class="code-name">#icon-duibi</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-huanyuan"></use>
|
||||
</svg>
|
||||
<div class="name">还原</div>
|
||||
<div class="code-name">#icon-huanyuan</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-vr"></use>
|
||||
</svg>
|
||||
<div class="name">vr</div>
|
||||
<div class="code-name">#icon-vr</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-chixushijian1"></use>
|
||||
</svg>
|
||||
<div class="name">持续时间</div>
|
||||
<div class="code-name">#icon-chixushijian1</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<div class="article markdown">
|
||||
<h2 id="symbol-">Symbol 引用</h2>
|
||||
<hr>
|
||||
|
||||
<p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
|
||||
这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:</p>
|
||||
<ul>
|
||||
<li>支持多色图标了,不再受单色限制。</li>
|
||||
<li>通过一些技巧,支持像字体那样,通过 <code>font-size</code>, <code>color</code> 来调整样式。</li>
|
||||
<li>兼容性较差,支持 IE9+,及现代浏览器。</li>
|
||||
<li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
|
||||
</ul>
|
||||
<p>使用步骤如下:</p>
|
||||
<h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
|
||||
<pre><code class="language-html"><script src="./iconfont.js"></script>
|
||||
</code></pre>
|
||||
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
|
||||
<pre><code class="language-html"><style>
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</code></pre>
|
||||
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
|
||||
<pre><code class="language-html"><svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-xxx"></use>
|
||||
</svg>
|
||||
</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('.tab-container .content:first').show()
|
||||
|
||||
$('#tabs li').click(function (e) {
|
||||
var tabContent = $('.tab-container .content')
|
||||
var index = $(this).index()
|
||||
|
||||
if ($(this).hasClass('active')) {
|
||||
return
|
||||
} else {
|
||||
$('#tabs li').removeClass('active')
|
||||
$(this).addClass('active')
|
||||
|
||||
tabContent.hide().eq(index).fadeIn()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
49
src/assets/style/font_oy0t1irww4k/iconfont.css
Normal file
@ -0,0 +1,49 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id */
|
||||
src: url('iconfont.ttf?t=1733881413198') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-quanping:before {
|
||||
content: "\e660";
|
||||
}
|
||||
|
||||
.icon-bangzhu:before {
|
||||
content: "\e620";
|
||||
}
|
||||
|
||||
.icon-bangzhu_o:before {
|
||||
content: "\eb72";
|
||||
}
|
||||
|
||||
.icon-bantouming:before {
|
||||
content: "\e60b";
|
||||
}
|
||||
|
||||
.icon-chixushijian:before {
|
||||
content: "\e71a";
|
||||
}
|
||||
|
||||
.icon-duibi:before {
|
||||
content: "\e60c";
|
||||
}
|
||||
|
||||
.icon-huanyuan:before {
|
||||
content: "\e604";
|
||||
}
|
||||
|
||||
.icon-vr:before {
|
||||
content: "\e70a";
|
||||
}
|
||||
|
||||
.icon-chixushijian1:before {
|
||||
content: "\e51b";
|
||||
}
|
||||
|
||||
1
src/assets/style/font_oy0t1irww4k/iconfont.js
Normal file
72
src/assets/style/font_oy0t1irww4k/iconfont.json
Normal file
@ -0,0 +1,72 @@
|
||||
{
|
||||
"id": "",
|
||||
"name": "",
|
||||
"font_family": "iconfont",
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "807275",
|
||||
"name": "全屏",
|
||||
"font_class": "quanping",
|
||||
"unicode": "e660",
|
||||
"unicode_decimal": 58976
|
||||
},
|
||||
{
|
||||
"icon_id": "1304879",
|
||||
"name": "帮助",
|
||||
"font_class": "bangzhu",
|
||||
"unicode": "e620",
|
||||
"unicode_decimal": 58912
|
||||
},
|
||||
{
|
||||
"icon_id": "5387852",
|
||||
"name": "帮助_o",
|
||||
"font_class": "bangzhu_o",
|
||||
"unicode": "eb72",
|
||||
"unicode_decimal": 60274
|
||||
},
|
||||
{
|
||||
"icon_id": "8883075",
|
||||
"name": "半透明(2)",
|
||||
"font_class": "bantouming",
|
||||
"unicode": "e60b",
|
||||
"unicode_decimal": 58891
|
||||
},
|
||||
{
|
||||
"icon_id": "11768105",
|
||||
"name": "持续时间",
|
||||
"font_class": "chixushijian",
|
||||
"unicode": "e71a",
|
||||
"unicode_decimal": 59162
|
||||
},
|
||||
{
|
||||
"icon_id": "15726777",
|
||||
"name": "对比",
|
||||
"font_class": "duibi",
|
||||
"unicode": "e60c",
|
||||
"unicode_decimal": 58892
|
||||
},
|
||||
{
|
||||
"icon_id": "16731254",
|
||||
"name": "还原",
|
||||
"font_class": "huanyuan",
|
||||
"unicode": "e604",
|
||||
"unicode_decimal": 58884
|
||||
},
|
||||
{
|
||||
"icon_id": "22273778",
|
||||
"name": "vr",
|
||||
"font_class": "vr",
|
||||
"unicode": "e70a",
|
||||
"unicode_decimal": 59146
|
||||
},
|
||||
{
|
||||
"icon_id": "42514699",
|
||||
"name": "持续时间",
|
||||
"font_class": "chixushijian1",
|
||||
"unicode": "e51b",
|
||||
"unicode_decimal": 58651
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
src/assets/style/font_oy0t1irww4k/iconfont.ttf
Normal file
104
src/assets/style/global.css
Normal file
@ -0,0 +1,104 @@
|
||||
/* 全局样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #1e1e1e;
|
||||
color: #cccccc;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #4a4a4a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #5a5a5a;
|
||||
}
|
||||
|
||||
/* 按钮基础样式 */
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* 输入框基础样式 */
|
||||
input, textarea, select {
|
||||
font-family: inherit;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #4a4a4a;
|
||||
color: #cccccc;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: #007acc;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '';
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid #4a4a4a;
|
||||
border-top: 3px solid #007acc;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 文本选择 */
|
||||
::selection {
|
||||
background: #007acc;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 禁用文本选择的元素 */
|
||||
.no-select {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
90
src/assets/style/loading.css
Normal file
@ -0,0 +1,90 @@
|
||||
.loading-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
background: url('/loading/加载页背景图.jpg') no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
width: 230px;
|
||||
max-width: 600px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: -70px;
|
||||
}
|
||||
|
||||
|
||||
.logo {
|
||||
width: 152px;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 50px;
|
||||
color: #333;
|
||||
position: relative;
|
||||
}
|
||||
.logo img{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.truck-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
margin-top: -40px; /* 向上移动,与进度条重叠 */
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background-color: rgba(255, 255, 255, 0.37);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
padding: 2px;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center; /* 保持垂直居中 */
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 4px; /* 保持这个高度 */
|
||||
background-color: #1087d6;
|
||||
transition: width 0.3s;
|
||||
border-radius: 3px;
|
||||
/* 移除这两行,它们导致了中间扩散效果 */
|
||||
/* margin: 0 auto; */
|
||||
/* align-self: center; */
|
||||
|
||||
/* 添加这行确保从左边开始 */
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.truck {
|
||||
position: absolute;
|
||||
top: -16px; /* 相对于truck-container定位 */
|
||||
transform: translateX(-90%); /* 改为-90%,让小车更靠左一些 */
|
||||
transition: left 0.3s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.truck img {
|
||||
display: block;
|
||||
width: 80px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
286
src/assets/style/media.css
Normal file
@ -0,0 +1,286 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
|
||||
|
||||
|
||||
/* 移动设备 */
|
||||
@media screen and (max-width: 350px) {
|
||||
.title {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 移动设备 */
|
||||
@media screen and (max-width: 640px) {
|
||||
|
||||
.loaderbox img {
|
||||
width: 200px !important;
|
||||
}
|
||||
|
||||
.jiudu {
|
||||
min-width: 70px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
|
||||
/* 横屏*/
|
||||
@media screen and (max-height: 766px) {
|
||||
|
||||
.ui-bottom {
|
||||
bottom: 20px;
|
||||
right: 10px;
|
||||
/* 通过偏移实现居中 */
|
||||
}
|
||||
|
||||
.dialog-item {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
}
|
||||
|
||||
.dialog-item .Bar-Line {
|
||||
width: calc(5rem + 6px);
|
||||
height: calc(5rem + 6px);
|
||||
box-shadow: 0 0 0 1px #fff;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 5px 5px
|
||||
}
|
||||
|
||||
.TopInfo-content .title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 竖屏*/
|
||||
@media screen and (min-height: 768px) {
|
||||
|
||||
.ui-bottom {
|
||||
bottom: 20px;
|
||||
right: 10px;
|
||||
/* 通过偏移实现居中 */
|
||||
}
|
||||
|
||||
.dialog-item {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
|
||||
}
|
||||
|
||||
.dialog-item .Bar-Line {
|
||||
width: calc(5rem + 6px);
|
||||
height: calc(5rem + 6px);
|
||||
box-shadow: 0 0 0 1px #fff;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 5px 5px
|
||||
}
|
||||
|
||||
|
||||
.TopInfo-content .title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.ColorBar1-container {
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
/* .ColorBar-container {
|
||||
|
||||
left: 0;
|
||||
top: 20px;
|
||||
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
} */
|
||||
}
|
||||
|
||||
|
||||
.return {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* 移动设备 */
|
||||
@media screen and (max-width: 767px) and (min-width: 640px) {
|
||||
|
||||
/* 横屏*/
|
||||
@media screen and (max-height: 400px) {
|
||||
|
||||
.ui-bottom {
|
||||
bottom: 20px;
|
||||
right: 10px;
|
||||
/* 通过偏移实现居中 */
|
||||
}
|
||||
|
||||
.dialog-item {
|
||||
width: 7rem;
|
||||
height: 7rem;
|
||||
}
|
||||
|
||||
.dialog-item .Bar-Line {
|
||||
width: calc(7rem + 6px);
|
||||
height: calc(7rem + 6px);
|
||||
box-shadow: 0 0 0 1px #fff;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 30px 10px !important;
|
||||
}
|
||||
|
||||
.dialog-bottom {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* 横屏*/
|
||||
@media screen and (max-height: 766px) and (min-height: 400px) {
|
||||
|
||||
.ui-bottom {
|
||||
bottom: 20px;
|
||||
right: 10px;
|
||||
/* 通过偏移实现居中 */
|
||||
}
|
||||
|
||||
.dialog-item {
|
||||
width: 9rem;
|
||||
height: 9rem;
|
||||
}
|
||||
|
||||
.dialog-item .Bar-Line {
|
||||
width: calc(9rem + 6px);
|
||||
height: calc(9rem + 6px);
|
||||
box-shadow: 0 0 0 1px #fff;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 5px 5px
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* 竖屏*/
|
||||
@media screen and (min-height: 768px) {
|
||||
|
||||
|
||||
.ui-bottom {
|
||||
bottom: 20px;
|
||||
right: 10px;
|
||||
/* 通过偏移实现居中 */
|
||||
}
|
||||
|
||||
.dialog-item {
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
|
||||
}
|
||||
|
||||
.dialog-item .Bar-Line {
|
||||
width: calc(10rem + 6px);
|
||||
height: calc(10rem + 6px);
|
||||
box-shadow: 0 0 0 1px #fff;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 5px 5px
|
||||
}
|
||||
|
||||
.TopInfo-content .title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.return {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* 电脑设备 */
|
||||
@media screen and (min-width: 768px) {
|
||||
|
||||
|
||||
|
||||
/* 横屏*/
|
||||
@media screen and (max-height: 400px) {
|
||||
|
||||
|
||||
|
||||
.ui-bottom {
|
||||
bottom: 20px;
|
||||
right: 10px;
|
||||
/* 通过偏移实现居中 */
|
||||
}
|
||||
|
||||
.dialog-item {
|
||||
width: 7rem;
|
||||
height: 7rem;
|
||||
}
|
||||
|
||||
.dialog-item .Bar-Line {
|
||||
width: calc(7rem + 6px);
|
||||
height: calc(7rem + 6px);
|
||||
box-shadow: 0 0 0 1px #fff;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
padding: 30px 10px !important;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* 横屏*/
|
||||
@media screen and (min-height: 400px) {
|
||||
|
||||
|
||||
.ui-bottom {
|
||||
right: 20px;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.dialog-item {
|
||||
width: 12.5rem;
|
||||
height: 12.5rem;
|
||||
}
|
||||
|
||||
.dialog-item .Bar-Line {
|
||||
|
||||
width: calc(12.5rem + 6px);
|
||||
height: calc(12.5rem + 6px);
|
||||
box-shadow: 0 0 0 1px #fff;
|
||||
}
|
||||
|
||||
.ColorBar1-container {
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
.jiudu {
|
||||
min-width: 5rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.return {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
}
|
||||
6
src/assets/style/normal.css
Normal file
@ -0,0 +1,6 @@
|
||||
html,
|
||||
body {
|
||||
overflow: hidden;
|
||||
margin : 0;
|
||||
padding : 0;
|
||||
}
|
||||
8
src/assets/style/style.css
Normal file
@ -0,0 +1,8 @@
|
||||
.property-label {
|
||||
width: 60px;
|
||||
color: #cccccc;
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
text-align: left;
|
||||
cursor: help;
|
||||
}
|
||||
414
src/components/BasicControls/Field/index.vue
Normal file
@ -0,0 +1,414 @@
|
||||
<template>
|
||||
<div class="field-container" :class="{
|
||||
'field-disabled': disabled,
|
||||
'field-drag-over': isDragOver,
|
||||
'field-has-value': hasValue
|
||||
}" @dragover.prevent="handleDragOver" @dragleave="handleDragLeave" @drop.prevent="handleDrop">
|
||||
<!-- 字段标签 -->
|
||||
<div class="field-label" v-if="label">
|
||||
{{ label }}
|
||||
</div>
|
||||
|
||||
<!-- 字段内容区域 -->
|
||||
<div class="field-content">
|
||||
<!-- 有值时显示的内容 -->
|
||||
<div v-if="hasValue" class="field-value">
|
||||
<!-- 图标 -->
|
||||
<div class="field-icon">
|
||||
<component :is="getIconComponent()" />
|
||||
</div>
|
||||
|
||||
<!-- 名称 -->
|
||||
<div class="field-name">
|
||||
{{ modelValue?.name || 'Unnamed' }}
|
||||
</div>
|
||||
|
||||
<!-- 清除按钮 -->
|
||||
<button class="field-clear" @click="clearValue" :disabled="disabled">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 空值时显示的占位符 -->
|
||||
<div v-else class="field-placeholder">
|
||||
<div class="field-icon-placeholder">
|
||||
<component :is="getIconComponent()" />
|
||||
</div>
|
||||
<div class="field-text">
|
||||
None ({{ acceptedType }})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 拖拽提示 -->
|
||||
<div v-if="isDragOver" class="field-drag-hint">
|
||||
<div class="drag-hint-text">
|
||||
{{ getDragHintText() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, h } from 'vue'
|
||||
import type { FieldProps, FieldEmits, FieldValue, AcceptedType } from './types'
|
||||
import { userDragStore } from 'stores/Filed'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useCursorStore } from '@/stores/Cursor'
|
||||
const dragStore = userDragStore()
|
||||
const cursorStore = useCursorStore()
|
||||
const { dragType, dragData } = storeToRefs(dragStore)
|
||||
|
||||
// 图标组件
|
||||
const TextureIcon = () => h('svg', {
|
||||
width: '16',
|
||||
height: '16',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'currentColor'
|
||||
}, [
|
||||
h('path', {
|
||||
d: 'M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z'
|
||||
})
|
||||
])
|
||||
|
||||
const MeshIcon = () => h('svg', {
|
||||
width: '16',
|
||||
height: '16',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'currentColor'
|
||||
}, [
|
||||
h('path', {
|
||||
d: 'M12,2L13.09,8.26L22,9L17.74,15.74L19,22L12,19L5,22L6.26,15.74L2,9L10.91,8.26L12,2Z'
|
||||
})
|
||||
])
|
||||
|
||||
const MaterialIcon = () => h('svg', {
|
||||
width: '16',
|
||||
height: '16',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'currentColor'
|
||||
}, [
|
||||
h('path', {
|
||||
d: 'M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z'
|
||||
})
|
||||
])
|
||||
|
||||
const props = withDefaults(defineProps<FieldProps>(), {
|
||||
disabled: false,
|
||||
acceptedType: 'Texture',
|
||||
allowClear: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<FieldEmits>()
|
||||
|
||||
// 状态
|
||||
const isDragOver = ref(false)
|
||||
|
||||
// 计算属性
|
||||
const hasValue = computed(() => props.modelValue !== null && props.modelValue !== undefined)
|
||||
|
||||
// 获取图标组件
|
||||
const getIconComponent = () => {
|
||||
switch (props.acceptedType) {
|
||||
case 'Texture':
|
||||
return TextureIcon
|
||||
case 'Mesh':
|
||||
return MeshIcon
|
||||
case 'Material':
|
||||
return MaterialIcon
|
||||
default:
|
||||
return TextureIcon
|
||||
}
|
||||
}
|
||||
|
||||
// 获取拖拽提示文本
|
||||
const getDragHintText = () => {
|
||||
return `Drop ${props.acceptedType} here`
|
||||
}
|
||||
|
||||
// 检查拖拽的元素是否为支持的类型
|
||||
const isValidDragItem = (dragData: any): boolean => {
|
||||
if (!dragData) return false
|
||||
|
||||
// 检查类型是否匹配
|
||||
if (dragData.type !== props.acceptedType) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 可以添加更多的验证逻辑
|
||||
// 例如检查文件扩展名、MIME类型等
|
||||
if (props.acceptedType === 'Texture') {
|
||||
const validExtensions = ['.png', '.jpg', '.jpeg', '.tga', '.bmp', '.gif']
|
||||
const fileName = dragData.name || ''
|
||||
return validExtensions.some(ext => fileName.toLowerCase().endsWith(ext))
|
||||
}
|
||||
|
||||
if (props.acceptedType === 'Mesh') {
|
||||
const validExtensions = ['.fbx', '.obj', '.dae', '.3ds', '.blend']
|
||||
const fileName = dragData.name || ''
|
||||
return validExtensions.some(ext => fileName.toLowerCase().endsWith(ext))
|
||||
}
|
||||
|
||||
if (props.acceptedType === 'Material') {
|
||||
const validExtensions = ['.mat', '.material']
|
||||
const fileName = dragData.name || ''
|
||||
return validExtensions.some(ext => fileName.toLowerCase().endsWith(ext))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// 拖拽事件处理
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
|
||||
if (props.disabled) return
|
||||
|
||||
try {
|
||||
const match = dragType.value == props.acceptedType
|
||||
if (match) {
|
||||
isDragOver.value = true
|
||||
cursorStore.setCursor('default')
|
||||
} else {
|
||||
cursorStore.setCursor('not-allowed')
|
||||
}
|
||||
} catch {
|
||||
// 如果无法解析数据,则拒绝拖拽
|
||||
cursorStore.setCursor('not-allowed')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
if (props.disabled) return
|
||||
|
||||
isDragOver.value = false
|
||||
|
||||
|
||||
try {
|
||||
const match = dragType.value == props.acceptedType
|
||||
if (match) {
|
||||
|
||||
if (isValidDragItem(dragData.value)) {
|
||||
const fieldValue: FieldValue = {
|
||||
id: dragData.value.id || Date.now().toString(),
|
||||
name: dragData.value.name || 'Unnamed',
|
||||
type: dragData.value.type,
|
||||
path: dragData.value.path || '',
|
||||
metadata: dragData.value.metadata || {}
|
||||
}
|
||||
|
||||
emit('update:modelValue', fieldValue)
|
||||
emit('change', fieldValue)
|
||||
} else {
|
||||
// 发出错误事件
|
||||
emit('error', {
|
||||
type: 'invalid_type',
|
||||
message: `Cannot assign ${dragData.value.type || 'unknown'} to ${props.acceptedType} field`,
|
||||
expectedType: props.acceptedType,
|
||||
actualType: dragData.value.type
|
||||
})
|
||||
}
|
||||
|
||||
} else {
|
||||
cursorStore.setCursor('not-allowed')
|
||||
}
|
||||
} catch {
|
||||
// 如果无法解析数据,则拒绝拖拽
|
||||
cursorStore.setCursor('not-allowed')
|
||||
}
|
||||
|
||||
// try {
|
||||
// const dragData = JSON.parse(event.dataTransfer?.getData('application/json') || '{}')
|
||||
|
||||
// if (isValidDragItem(dragData)) {
|
||||
// const fieldValue: FieldValue = {
|
||||
// id: dragData.id || Date.now().toString(),
|
||||
// name: dragData.name || 'Unnamed',
|
||||
// type: dragData.type,
|
||||
// path: dragData.path || '',
|
||||
// metadata: dragData.metadata || {}
|
||||
// }
|
||||
|
||||
// emit('update:modelValue', fieldValue)
|
||||
// emit('change', fieldValue)
|
||||
// } else {
|
||||
// // 发出错误事件
|
||||
// emit('error', {
|
||||
// type: 'invalid_type',
|
||||
// message: `Cannot assign ${dragData.type || 'unknown'} to ${props.acceptedType} field`,
|
||||
// expectedType: props.acceptedType,
|
||||
// actualType: dragData.type
|
||||
// })
|
||||
// }
|
||||
// } catch (error) {
|
||||
// emit('error', {
|
||||
// type: 'parse_error',
|
||||
// message: 'Failed to parse drag data',
|
||||
// error
|
||||
// })
|
||||
// }
|
||||
}
|
||||
|
||||
// 清除值
|
||||
const clearValue = () => {
|
||||
if (props.disabled || !props.allowClear) return
|
||||
|
||||
emit('update:modelValue', null)
|
||||
emit('change', null)
|
||||
emit('clear')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.field-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
color: #cccccc;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.field-content {
|
||||
position: relative;
|
||||
min-height: 20px;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.field-container.field-drag-over .field-content {
|
||||
border-color: #409eff;
|
||||
background: #353535;
|
||||
box-shadow: 0 0 0 1px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
|
||||
.field-container.field-disabled .field-content {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.field-container.field-has-value .field-content {
|
||||
border-color: #5a5a5a;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 3px 6px;
|
||||
gap: 6px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.field-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 3px 6px;
|
||||
gap: 6px;
|
||||
height: 100%;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.field-icon,
|
||||
.field-icon-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.field-has-value .field-icon {
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.field-name {
|
||||
flex: 1;
|
||||
color: #cccccc;
|
||||
font-size: 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.field-text {
|
||||
flex: 1;
|
||||
font-size: 10px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.field-clear {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #888888;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 1px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.field-clear:hover {
|
||||
background: #4a4a4a;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.field-clear:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.field-drag-hint {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(64, 158, 255, 0.1);
|
||||
border: 2px dashed #409eff;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.drag-hint-text {
|
||||
color: #409eff;
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 悬停效果 */
|
||||
.field-content:hover {
|
||||
border-color: #5a5a5a;
|
||||
}
|
||||
|
||||
.field-container.field-disabled .field-content:hover {
|
||||
border-color: #4a4a4a;
|
||||
}
|
||||
|
||||
/* 焦点效果 */
|
||||
.field-content:focus-within {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 0 0 1px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
34
src/components/BasicControls/Field/types.ts
Normal file
@ -0,0 +1,34 @@
|
||||
// Field 组件类型定义
|
||||
|
||||
export type AcceptedType = 'Texture' | 'Mesh' | 'Material'
|
||||
|
||||
export interface FieldValue {
|
||||
id: string
|
||||
name: string
|
||||
type: AcceptedType
|
||||
path: string
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface FieldError {
|
||||
type: 'invalid_type' | 'parse_error'
|
||||
message: string
|
||||
expectedType?: AcceptedType
|
||||
actualType?: string
|
||||
error?: any
|
||||
}
|
||||
|
||||
export interface FieldProps {
|
||||
modelValue?: FieldValue | null
|
||||
label?: string
|
||||
acceptedType?: AcceptedType
|
||||
disabled?: boolean
|
||||
allowClear?: boolean
|
||||
}
|
||||
|
||||
export interface FieldEmits {
|
||||
'update:modelValue': [value: FieldValue | null]
|
||||
'change': [value: FieldValue | null]
|
||||
'clear': []
|
||||
'error': [error: FieldError]
|
||||
}
|
||||
163
src/components/BasicControls/FileNode/index.css
Normal file
@ -0,0 +1,163 @@
|
||||
/* FileNode 组件样式 - 文件夹形式 */
|
||||
.file-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 2px 2px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
/* background: #353535; */
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.file-node:hover {
|
||||
background-color: #404040;
|
||||
}
|
||||
|
||||
.file-node.dragging {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.file-node.drag-over {
|
||||
background-color: rgba(64, 158, 255, 0.2);
|
||||
border: 2px dashed #409eff;
|
||||
}
|
||||
|
||||
/* 拖拽指示器 */
|
||||
.file-node.drag-over-before::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background-color: #409eff;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.file-node.drag-over-after::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background-color: #409eff;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* 文件图标 */
|
||||
.file-node-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
margin-bottom: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 文件名 */
|
||||
.file-node-name {
|
||||
font-size: 11px;
|
||||
color: #cccccc;
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
line-height: 1.2;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* 选中效果只应用到文件名 */
|
||||
.file-node.selected .file-node-name {
|
||||
background-color: #2e70b3b7;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.file-node-name.editing {
|
||||
/* background-color: #2d2d2d;
|
||||
border: 1px solid #409eff; */
|
||||
border-radius: 2px;
|
||||
padding: 2px 4px;
|
||||
outline: none;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 文件大小 */
|
||||
.file-node-size {
|
||||
font-size: 10px;
|
||||
color: #888;
|
||||
margin-top: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 拖拽鼠标样式 */
|
||||
.drag-cursor-grab {
|
||||
cursor: grab !important;
|
||||
}
|
||||
|
||||
.drag-cursor-grabbing {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
|
||||
.drag-cursor-copy {
|
||||
cursor: copy !important;
|
||||
}
|
||||
|
||||
.drag-cursor-move {
|
||||
cursor: move !important;
|
||||
}
|
||||
|
||||
.drag-cursor-not-allowed {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.file-node {
|
||||
padding: 8px 6px;
|
||||
min-height: 70px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.file-node-icon {
|
||||
font-size: 28px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.file-node-name {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.file-node-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.file-node-container::-webkit-scrollbar-track {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
.file-node-container::-webkit-scrollbar-thumb {
|
||||
background: #4a4a4a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.file-node-container::-webkit-scrollbar-thumb:hover {
|
||||
background: #5a5a5a;
|
||||
}
|
||||
5
src/components/BasicControls/FileNode/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// 导出类型和常量
|
||||
export * from './types'
|
||||
|
||||
// 导出组件
|
||||
export { default } from './index.vue'
|
||||
258
src/components/BasicControls/FileNode/index.vue
Normal file
@ -0,0 +1,258 @@
|
||||
<template>
|
||||
<div
|
||||
:class="nodeClasses"
|
||||
:style="nodeStyles"
|
||||
@click="handleClick"
|
||||
@dblclick="handleDoubleClick"
|
||||
@dragstart="handleDragStart"
|
||||
@dragend="handleDragEnd"
|
||||
@dragover="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop"
|
||||
:draggable="draggable"
|
||||
>
|
||||
<!-- 文件图标 -->
|
||||
<div class="file-node-icon">
|
||||
{{ FILE_ICONS[node.type] }}
|
||||
</div>
|
||||
|
||||
<!-- 文件名 -->
|
||||
<div
|
||||
v-if="!isEditing"
|
||||
class="file-node-name"
|
||||
@dblclick.stop="startEdit"
|
||||
>
|
||||
{{ node.name }}
|
||||
</div>
|
||||
<input
|
||||
v-else
|
||||
ref="editInput"
|
||||
v-model="editName"
|
||||
class="file-node-name editing"
|
||||
@blur="finishEdit"
|
||||
@keyup.enter="finishEdit"
|
||||
@keyup.esc="cancelEdit"
|
||||
@click.stop
|
||||
/>
|
||||
|
||||
<!-- 文件大小(可选显示) -->
|
||||
<div
|
||||
v-if="showSize && node.size !== undefined"
|
||||
class="file-node-size"
|
||||
>
|
||||
{{ formatFileSize(node.size) }}
|
||||
</div>
|
||||
|
||||
<!-- 修改时间 -->
|
||||
<div
|
||||
v-if="showDetails && node.lastModified"
|
||||
class="file-node-date"
|
||||
>
|
||||
{{ formatDate(node.lastModified) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick, watch } from 'vue'
|
||||
import { FileNode, FileNodeType, FILE_ICONS, DRAG_CURSORS } from './types'
|
||||
import { formatFileSize, formatDate } from '../../../utils/Tools'
|
||||
import './index.css'
|
||||
|
||||
interface Props {
|
||||
node: FileNode
|
||||
depth?: number
|
||||
draggable?: boolean
|
||||
showDetails?: boolean
|
||||
selectedId?: string
|
||||
dragState?: {
|
||||
isDragging: boolean
|
||||
dragNode: FileNode | null
|
||||
dragOverNode: FileNode | null
|
||||
dropPosition: 'before' | 'after' | 'inside' | null
|
||||
}
|
||||
showSize?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'click', node: FileNode): void
|
||||
(e: 'double-click', node: FileNode): void
|
||||
(e: 'expand', node: FileNode): void
|
||||
(e: 'rename', node: FileNode, newName: string): void
|
||||
(e: 'drag-start', node: FileNode, event: DragEvent): void
|
||||
(e: 'drag-end', node: FileNode, event: DragEvent): void
|
||||
(e: 'drag-over', node: FileNode, event: DragEvent): void
|
||||
(e: 'drag-leave', node: FileNode, event: DragEvent): void
|
||||
(e: 'drop', targetNode: FileNode, dragNode: FileNode, position: string, event: DragEvent): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
depth: 0,
|
||||
draggable: true,
|
||||
showDetails: false,
|
||||
showSize: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 编辑状态
|
||||
const isEditing = ref(false)
|
||||
const editName = ref('')
|
||||
const editInput = ref<HTMLInputElement>()
|
||||
|
||||
// 计算属性
|
||||
const hasChildren = computed(() => {
|
||||
return props.node.children && props.node.children.length > 0
|
||||
})
|
||||
|
||||
const nodeClasses = computed(() => {
|
||||
const classes = [
|
||||
'file-node',
|
||||
`type-${props.node.type.toLowerCase()}`
|
||||
]
|
||||
|
||||
if (props.node.isSelected || props.selectedId === props.node.id) {
|
||||
classes.push('selected')
|
||||
}
|
||||
|
||||
if (props.node.isDragging) {
|
||||
classes.push('dragging')
|
||||
}
|
||||
|
||||
if (props.dragState?.dragOverNode?.id === props.node.id) {
|
||||
classes.push('drag-over')
|
||||
if (props.dragState.dropPosition === 'before') {
|
||||
classes.push('drag-over-before')
|
||||
} else if (props.dragState.dropPosition === 'after') {
|
||||
classes.push('drag-over-after')
|
||||
}
|
||||
}
|
||||
|
||||
return classes
|
||||
})
|
||||
|
||||
const nodeStyles = computed(() => {
|
||||
const styles: Record<string, string> = {}
|
||||
|
||||
if (props.dragState?.isDragging) {
|
||||
if (props.dragState.dragNode?.id === props.node.id) {
|
||||
styles.cursor = DRAG_CURSORS.GRABBING
|
||||
} else {
|
||||
styles.cursor = DRAG_CURSORS.GRAB
|
||||
}
|
||||
}
|
||||
|
||||
return styles
|
||||
})
|
||||
|
||||
// 事件处理
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (!isEditing.value) {
|
||||
emit('click', props.node)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDoubleClick = (event: MouseEvent) => {
|
||||
if (!isEditing.value) {
|
||||
emit('double-click', props.node)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleExpand = () => {
|
||||
if (props.node.type === FileNodeType.Folder) {
|
||||
emit('expand', props.node)
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑功能
|
||||
const startEdit = () => {
|
||||
if (props.node.type !== FileNodeType.Folder) {
|
||||
isEditing.value = true
|
||||
editName.value = props.node.name
|
||||
nextTick(() => {
|
||||
editInput.value?.focus()
|
||||
editInput.value?.select()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const finishEdit = () => {
|
||||
if (editName.value.trim() && editName.value !== props.node.name) {
|
||||
emit('rename', props.node, editName.value.trim())
|
||||
}
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
editName.value = props.node.name
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
// 拖拽功能
|
||||
const handleDragStart = (event: DragEvent) => {
|
||||
if (!props.draggable) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
event.dataTransfer!.effectAllowed = 'move'
|
||||
event.dataTransfer!.setData('text/plain', props.node.id)
|
||||
|
||||
// 设置拖拽图像
|
||||
const dragImage = event.currentTarget as HTMLElement
|
||||
event.dataTransfer!.setDragImage(dragImage, 0, 0)
|
||||
|
||||
emit('drag-start', props.node, event)
|
||||
}
|
||||
|
||||
const handleDragEnd = (event: DragEvent) => {
|
||||
emit('drag-end', props.node, event)
|
||||
}
|
||||
|
||||
const handleDragOver = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
event.dataTransfer!.dropEffect = 'move'
|
||||
|
||||
// 计算拖拽位置
|
||||
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
|
||||
const y = event.clientY - rect.top
|
||||
const height = rect.height
|
||||
|
||||
let position: 'before' | 'after' | 'inside' = 'inside'
|
||||
|
||||
if (props.node.type === FileNodeType.Folder) {
|
||||
if (y < height * 0.25) {
|
||||
position = 'before'
|
||||
} else if (y > height * 0.75) {
|
||||
position = 'after'
|
||||
} else {
|
||||
position = 'inside'
|
||||
}
|
||||
} else {
|
||||
position = y < height * 0.5 ? 'before' : 'after'
|
||||
}
|
||||
|
||||
emit('drag-over', props.node, event)
|
||||
}
|
||||
|
||||
const handleDragLeave = (event: DragEvent) => {
|
||||
emit('drag-leave', props.node, event)
|
||||
}
|
||||
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
|
||||
const dragNodeId = event.dataTransfer!.getData('text/plain')
|
||||
if (dragNodeId && props.dragState?.dragNode) {
|
||||
const position = props.dragState.dropPosition || 'inside'
|
||||
emit('drop', props.node, props.dragState.dragNode, position, event)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听选中状态变化
|
||||
watch(() => props.selectedId, (newId) => {
|
||||
if (newId !== props.node.id && isEditing.value) {
|
||||
cancelEdit()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
82
src/components/BasicControls/FileNode/types.ts
Normal file
@ -0,0 +1,82 @@
|
||||
// 文件节点类型枚举
|
||||
export enum FileNodeType {
|
||||
Text = 'Text',
|
||||
Folder = 'Folder',
|
||||
Model = 'Model',
|
||||
Audio = 'Audio',
|
||||
Video = 'Video',
|
||||
Material = 'Material',
|
||||
Texture = 'Texture',
|
||||
Image = 'Image',
|
||||
Script = 'Script',
|
||||
Scene = 'Scene'
|
||||
}
|
||||
|
||||
// 文件节点接口
|
||||
export interface FileNode {
|
||||
id: string
|
||||
name: string
|
||||
type: FileNodeType
|
||||
path: string
|
||||
size?: number
|
||||
lastModified?: Date
|
||||
children?: FileNode[]
|
||||
parent?: string
|
||||
isExpanded?: boolean
|
||||
isSelected?: boolean
|
||||
isDragging?: boolean
|
||||
}
|
||||
|
||||
// 拖拽状态
|
||||
export interface DragState {
|
||||
isDragging: boolean
|
||||
dragNode: FileNode | null
|
||||
dragOverNode: FileNode | null
|
||||
dropPosition: 'before' | 'after' | 'inside' | null
|
||||
}
|
||||
|
||||
// 文件图标映射
|
||||
export const FILE_ICONS: Record<FileNodeType, string> = {
|
||||
[FileNodeType.Text]: '📄',
|
||||
[FileNodeType.Folder]: '📁',
|
||||
[FileNodeType.Model]: '🎯',
|
||||
[FileNodeType.Audio]: '🎵',
|
||||
[FileNodeType.Video]: '🎬',
|
||||
[FileNodeType.Material]: '🎨',
|
||||
[FileNodeType.Texture]: '🖼️',
|
||||
[FileNodeType.Image]: '🖼️',
|
||||
[FileNodeType.Script]: '📜',
|
||||
[FileNodeType.Scene]: '🌍'
|
||||
}
|
||||
|
||||
// 文件扩展名映射
|
||||
export const FILE_EXTENSIONS: Record<string, FileNodeType> = {
|
||||
'.txt': FileNodeType.Text,
|
||||
'.md': FileNodeType.Text,
|
||||
'.js': FileNodeType.Script,
|
||||
'.ts': FileNodeType.Script,
|
||||
'.vue': FileNodeType.Script,
|
||||
'.fbx': FileNodeType.Model,
|
||||
'.obj': FileNodeType.Model,
|
||||
'.gltf': FileNodeType.Model,
|
||||
'.glb': FileNodeType.Model,
|
||||
'.mp3': FileNodeType.Audio,
|
||||
'.wav': FileNodeType.Audio,
|
||||
'.mp4': FileNodeType.Video,
|
||||
'.avi': FileNodeType.Video,
|
||||
'.png': FileNodeType.Image,
|
||||
'.jpg': FileNodeType.Image,
|
||||
'.jpeg': FileNodeType.Image,
|
||||
'.gif': FileNodeType.Image,
|
||||
'.mat': FileNodeType.Material,
|
||||
'.material': FileNodeType.Material
|
||||
}
|
||||
|
||||
// 拖拽鼠标样式
|
||||
export const DRAG_CURSORS = {
|
||||
GRAB: 'grab',
|
||||
GRABBING: 'grabbing',
|
||||
COPY: 'copy',
|
||||
MOVE: 'move',
|
||||
NOT_ALLOWED: 'not-allowed'
|
||||
}
|
||||
104
src/components/BasicControls/Input/ColorInput.vue
Normal file
@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="color-input">
|
||||
<div class="color-preview" :style="{ backgroundColor: modelValue }" @click="openColorPicker"></div>
|
||||
<input
|
||||
ref="colorInput"
|
||||
type="color"
|
||||
:value="modelValue"
|
||||
@input="updateColor"
|
||||
class="color-picker"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
:value="modelValue"
|
||||
@input="updateColorText"
|
||||
@blur="validateColor"
|
||||
class="color-text"
|
||||
placeholder="#ffffff"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 本地类型定义
|
||||
interface ColorInputProps {
|
||||
modelValue: string
|
||||
}
|
||||
|
||||
interface ColorInputEmits {
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}
|
||||
|
||||
const props = defineProps<ColorInputProps>()
|
||||
const emit = defineEmits<ColorInputEmits>()
|
||||
|
||||
const colorInput = ref<HTMLInputElement>()
|
||||
|
||||
const openColorPicker = () => {
|
||||
colorInput.value?.click()
|
||||
}
|
||||
|
||||
const updateColor = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:modelValue', target.value)
|
||||
}
|
||||
|
||||
const updateColorText = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
emit('update:modelValue', target.value)
|
||||
}
|
||||
|
||||
const validateColor = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const value = target.value
|
||||
|
||||
// 验证颜色格式
|
||||
if (!/^#[0-9A-Fa-f]{6}$/.test(value)) {
|
||||
target.value = props.modelValue
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.color-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid #333333;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.color-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 2px 4px;
|
||||
background: #2a2a2a;
|
||||
border: 1px solid #333333;
|
||||
border-radius: 2px;
|
||||
color: #ffffff;
|
||||
font-size: 11px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.color-text:focus {
|
||||
outline: none;
|
||||
border-color: #007acc;
|
||||
}
|
||||
</style>
|
||||
71
src/components/BasicControls/Input/TextInput.vue
Normal file
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<input
|
||||
type="text"
|
||||
:value="modelValue"
|
||||
@input="handleInput"
|
||||
@focus="onFocus"
|
||||
:placeholder="placeholder"
|
||||
:class="['text-input', { 'center': textAlign === 'center', 'uppercase': uppercase }]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TextInputProps, TextInputEmits } from './types'
|
||||
|
||||
const props = withDefaults(defineProps<TextInputProps>(), {
|
||||
textAlign: 'left',
|
||||
uppercase: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<TextInputEmits>()
|
||||
|
||||
const handleInput = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
emit('update:modelValue', target.value)
|
||||
}
|
||||
|
||||
const onFocus = (e: Event) => {
|
||||
(e.target as HTMLInputElement).select()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-input {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 2px;
|
||||
color: #cccccc;
|
||||
font-size: 11px;
|
||||
padding: 0 3px;
|
||||
text-align: left;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.text-input.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-input.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.text-input:focus {
|
||||
border-color: #409eff;
|
||||
background: #353535;
|
||||
}
|
||||
|
||||
.text-input:hover {
|
||||
border-color: #5a5a5a;
|
||||
}
|
||||
|
||||
/* 容器自适应 */
|
||||
@container (max-width: 200px) {
|
||||
.text-input {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
92
src/components/BasicControls/Input/index.vue
Normal file
@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<input
|
||||
type="number"
|
||||
:value="modelValue"
|
||||
@input="handleInput"
|
||||
@focus="onFocus"
|
||||
:step="step"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:placeholder="placeholder"
|
||||
:class="['number-input', { 'full-width': fullWidth }]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { NumberInputProps, NumberInputEmits } from './types'
|
||||
|
||||
const props = withDefaults(defineProps<NumberInputProps>(), {
|
||||
step: 0.1,
|
||||
fullWidth: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<NumberInputEmits>()
|
||||
|
||||
const handleInput = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
const value = parseFloat(target.value)
|
||||
if (!isNaN(value)) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
}
|
||||
|
||||
const onFocus = (e: Event) => {
|
||||
(e.target as HTMLInputElement).select()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.number-input {
|
||||
width: 100%;
|
||||
min-width: 30px;
|
||||
|
||||
height: 16px;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 2px;
|
||||
color: #cccccc;
|
||||
font-size: 11px;
|
||||
padding: 0 3px;
|
||||
text-align: right;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.number-input.full-width {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.number-input:focus {
|
||||
border-color: #409eff;
|
||||
background: #353535;
|
||||
}
|
||||
|
||||
.number-input:hover {
|
||||
border-color: #5a5a5a;
|
||||
}
|
||||
|
||||
/* 隐藏数字输入框的箭头 */
|
||||
.number-input::-webkit-outer-spin-button,
|
||||
.number-input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.number-input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
/* 容器自适应 */
|
||||
@container (max-width: 200px) {
|
||||
.number-input {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 250px) {
|
||||
.number-input {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
src/components/BasicControls/Input/types.ts
Normal file
@ -0,0 +1,34 @@
|
||||
// NumberInput 组件类型
|
||||
export interface NumberInputProps {
|
||||
modelValue: number
|
||||
step?: number
|
||||
min?: number
|
||||
max?: number
|
||||
placeholder?: string
|
||||
fullWidth?: boolean
|
||||
}
|
||||
|
||||
export interface NumberInputEmits {
|
||||
(e: 'update:modelValue', value: number): void
|
||||
}
|
||||
|
||||
// TextInput 组件类型
|
||||
export interface TextInputProps {
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
textAlign?: 'left' | 'center' | 'right'
|
||||
uppercase?: boolean
|
||||
}
|
||||
|
||||
export interface TextInputEmits {
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}
|
||||
|
||||
// ColorInput 组件类型
|
||||
export interface ColorInputProps {
|
||||
modelValue: string
|
||||
}
|
||||
|
||||
export interface ColorInputEmits {
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}
|
||||
69
src/components/BasicControls/PropertyRow/index.vue
Normal file
@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="property-row">
|
||||
<Tooltip v-if="tooltip" :content="tooltip" placement="top">
|
||||
<div class="property-label">{{ label }}</div>
|
||||
</Tooltip>
|
||||
<div v-else class="property-label">{{ label }}</div>
|
||||
<div class="property-content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Tooltip from '../../public/Tooltip.vue'
|
||||
import type { PropertyRowProps } from './types'
|
||||
|
||||
defineProps<PropertyRowProps>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.property-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
min-height: 18px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.property-label {
|
||||
width: 80px;
|
||||
color: #cccccc;
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.property-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 250px) {
|
||||
.property-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.property-label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.property-content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 容器自适应 */
|
||||
@container (max-width: 200px) {
|
||||
.property-label {
|
||||
width: 60px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
5
src/components/BasicControls/PropertyRow/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// PropertyRow 组件类型定义
|
||||
export interface PropertyRowProps {
|
||||
label: string
|
||||
tooltip?: string
|
||||
}
|
||||
66
src/components/BasicControls/Select/index.vue
Normal file
@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<select
|
||||
:value="modelValue"
|
||||
@change="handleChange"
|
||||
class="select-input"
|
||||
>
|
||||
<option
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { SelectProps, SelectEmits } from './types'
|
||||
|
||||
defineProps<SelectProps>()
|
||||
const emit = defineEmits<SelectEmits>()
|
||||
|
||||
const handleChange = (e: Event) => {
|
||||
const target = e.target as HTMLSelectElement
|
||||
const value = target.value
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.select-input {
|
||||
width: 100%;
|
||||
height: 18px;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 2px;
|
||||
color: #cccccc;
|
||||
font-size: 11px;
|
||||
padding: 0 3px;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.select-input option {
|
||||
background: #2d2d2d;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.select-input:focus {
|
||||
border-color: #409eff;
|
||||
background: #353535;
|
||||
}
|
||||
|
||||
.select-input:hover {
|
||||
border-color: #5a5a5a;
|
||||
}
|
||||
|
||||
/* 容器自适应 */
|
||||
@container (max-width: 200px) {
|
||||
.select-input {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
15
src/components/BasicControls/Select/types.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// Select 组件类型定义
|
||||
export interface SelectOption {
|
||||
value: string | number
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface SelectProps {
|
||||
modelValue: string | number
|
||||
options: SelectOption[]
|
||||
}
|
||||
|
||||
export interface SelectEmits {
|
||||
(e: 'update:modelValue', value: string | number): void
|
||||
(e: 'change', value: string | number): void
|
||||
}
|
||||
326
src/components/BasicControls/Slider/index.vue
Normal file
@ -0,0 +1,326 @@
|
||||
<template>
|
||||
<div :class="['slider-wrapper', { 'slider-vertical': vertical }]">
|
||||
<div
|
||||
ref="sliderTrack"
|
||||
:class="['slider-track', { 'slider-track-vertical': vertical }]"
|
||||
@mousedown="handleMouseDown"
|
||||
>
|
||||
<div
|
||||
:class="['slider-fill', { 'slider-fill-vertical': vertical, 'no-transition': isDragging }]"
|
||||
:style="fillStyle"
|
||||
></div>
|
||||
<div
|
||||
ref="sliderThumb"
|
||||
:class="['slider-thumb', { 'slider-thumb-vertical': vertical, 'dragging': isDragging }]"
|
||||
:style="thumbStyle"
|
||||
@mousedown="handleThumbMouseDown"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- 数值显示 -->
|
||||
<div v-if="showValue && !showInput" class="slider-value">{{ formatValue(modelValue) }}</div>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<NumberInput
|
||||
v-if="showInput"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="handleInputChange"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
class="slider-input"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import NumberInput from '../Input/index.vue'
|
||||
import type { SliderProps, SliderEmits } from './types'
|
||||
|
||||
const props = withDefaults(defineProps<SliderProps>(), {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
showValue: false,
|
||||
showInput: true,
|
||||
vertical: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<SliderEmits>()
|
||||
|
||||
const sliderTrack = ref<HTMLElement>()
|
||||
const sliderThumb = ref<HTMLElement>()
|
||||
const isDragging = ref(false)
|
||||
|
||||
// 计算百分比
|
||||
const percentage = computed(() => {
|
||||
const range = props.max - props.min
|
||||
if (range === 0) return 0
|
||||
return ((props.modelValue - props.min) / range) * 100
|
||||
})
|
||||
|
||||
// 填充样式
|
||||
const fillStyle = computed(() => {
|
||||
if (props.vertical) {
|
||||
return { height: `${percentage.value}%` }
|
||||
} else {
|
||||
return { width: `${percentage.value}%` }
|
||||
}
|
||||
})
|
||||
|
||||
// 滑块样式
|
||||
const thumbStyle = computed(() => {
|
||||
if (props.vertical) {
|
||||
return { bottom: `calc(${percentage.value}% - 6px)` }
|
||||
} else {
|
||||
return { left: `calc(${percentage.value}% - 6px)` }
|
||||
}
|
||||
})
|
||||
|
||||
// 格式化显示值
|
||||
const formatValue = (value: number) => {
|
||||
if (props.step < 1) {
|
||||
return value.toFixed(2)
|
||||
} else if (props.step < 0.1) {
|
||||
return value.toFixed(3)
|
||||
} else {
|
||||
return Math.round(value).toString()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理输入框数值变化
|
||||
const handleInputChange = (value: number) => {
|
||||
// 确保值在范围内
|
||||
const clampedValue = Math.max(props.min, Math.min(props.max, value))
|
||||
|
||||
if (clampedValue !== props.modelValue) {
|
||||
emit('update:modelValue', clampedValue)
|
||||
emit('change', clampedValue)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理轨道鼠标按下
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
// 如果点击的是滑块,不处理
|
||||
if (e.target === sliderThumb.value) return
|
||||
|
||||
isDragging.value = true
|
||||
updateValue(e)
|
||||
|
||||
// 使用 requestAnimationFrame 优化性能
|
||||
const handleMove = (e: MouseEvent) => {
|
||||
if (!isDragging.value) return
|
||||
requestAnimationFrame(() => updateValue(e))
|
||||
}
|
||||
|
||||
const handleUp = (e: MouseEvent) => {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('mousemove', handleMove)
|
||||
document.removeEventListener('mouseup', handleUp)
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', handleMove)
|
||||
document.addEventListener('mouseup', handleUp)
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
// 处理滑块鼠标按下
|
||||
const handleThumbMouseDown = (e: MouseEvent) => {
|
||||
isDragging.value = true
|
||||
|
||||
// 记录初始偏移量
|
||||
const rect = sliderTrack.value!.getBoundingClientRect()
|
||||
const thumbRect = sliderThumb.value!.getBoundingClientRect()
|
||||
|
||||
let offset = 0
|
||||
if (props.vertical) {
|
||||
offset = e.clientY - (thumbRect.top + thumbRect.height / 2)
|
||||
} else {
|
||||
offset = e.clientX - (thumbRect.left + thumbRect.width / 2)
|
||||
}
|
||||
|
||||
const handleMove = (e: MouseEvent) => {
|
||||
if (!isDragging.value) return
|
||||
|
||||
// 调整鼠标位置以考虑初始偏移
|
||||
const adjustedEvent = {
|
||||
...e,
|
||||
clientX: props.vertical ? e.clientX : e.clientX - offset,
|
||||
clientY: props.vertical ? e.clientY - offset : e.clientY
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => updateValue(adjustedEvent as MouseEvent))
|
||||
}
|
||||
|
||||
const handleUp = (e: MouseEvent) => {
|
||||
isDragging.value = false
|
||||
document.removeEventListener('mousemove', handleMove)
|
||||
document.removeEventListener('mouseup', handleUp)
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', handleMove)
|
||||
document.addEventListener('mouseup', handleUp)
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
// 更新值
|
||||
const updateValue = (e: MouseEvent) => {
|
||||
if (!sliderTrack.value) return
|
||||
|
||||
const rect = sliderTrack.value.getBoundingClientRect()
|
||||
let percentage: number
|
||||
|
||||
if (props.vertical) {
|
||||
percentage = (rect.bottom - e.clientY) / rect.height
|
||||
} else {
|
||||
percentage = (e.clientX - rect.left) / rect.width
|
||||
}
|
||||
|
||||
// 限制百分比在 0-1 之间
|
||||
percentage = Math.max(0, Math.min(1, percentage))
|
||||
|
||||
// 计算新值
|
||||
const range = props.max - props.min
|
||||
let newValue = props.min + percentage * range
|
||||
|
||||
// 应用步长
|
||||
if (props.step > 0) {
|
||||
newValue = Math.round(newValue / props.step) * props.step
|
||||
}
|
||||
|
||||
// 确保值在范围内
|
||||
newValue = Math.max(props.min, Math.min(props.max, newValue))
|
||||
|
||||
// 修复浮点数精度问题
|
||||
if (props.step < 1) {
|
||||
const decimals = props.step.toString().split('.')[1]?.length || 0
|
||||
newValue = parseFloat(newValue.toFixed(decimals))
|
||||
}
|
||||
|
||||
// 减少不必要的更新
|
||||
if (Math.abs(newValue - props.modelValue) > 0.0001) {
|
||||
emit('update:modelValue', newValue)
|
||||
emit('change', newValue)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.slider-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.slider-wrapper.slider-vertical {
|
||||
flex-direction: column;
|
||||
height: 100px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.slider-track {
|
||||
position: relative;
|
||||
height: 4px;
|
||||
background: #4a4a4a;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.slider-track.slider-track-vertical {
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.slider-fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: #409eff;
|
||||
border-radius: 2px;
|
||||
transition: width 0.1s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.slider-fill.no-transition {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.slider-fill.slider-fill-vertical {
|
||||
bottom: 0;
|
||||
top: auto;
|
||||
width: 100%;
|
||||
transition: height 0.1s ease;
|
||||
}
|
||||
|
||||
.slider-fill.slider-fill-vertical.no-transition {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.slider-thumb {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #ffffff;
|
||||
border: 2px solid #409eff;
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: grab;
|
||||
transition: all 0.1s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.slider-thumb.dragging {
|
||||
transition: none;
|
||||
cursor: grabbing;
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
.slider-thumb.slider-thumb-vertical {
|
||||
left: 50%;
|
||||
top: auto;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.slider-thumb.slider-thumb-vertical.dragging {
|
||||
transform: translateX(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
.slider-thumb:hover:not(.dragging) {
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.slider-thumb.slider-thumb-vertical:hover:not(.dragging) {
|
||||
transform: translateX(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
.slider-thumb:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
min-width: 30px;
|
||||
color: #cccccc;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.slider-input {
|
||||
width: 60px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
16
src/components/BasicControls/Slider/types.ts
Normal file
@ -0,0 +1,16 @@
|
||||
// Slider 组件类型定义
|
||||
export interface SliderProps {
|
||||
modelValue: number
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
showValue?: boolean
|
||||
showInput?: boolean
|
||||
vertical?: boolean
|
||||
a?:number
|
||||
}
|
||||
|
||||
export interface SliderEmits {
|
||||
(e: 'update:modelValue', value: number): void
|
||||
(e: 'change', value: number): void
|
||||
}
|
||||
155
src/components/BasicControls/Switch/index.vue
Normal file
@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="switch-wrapper" :class="{ 'switch-disabled': disabled }">
|
||||
<input
|
||||
type="checkbox"
|
||||
:id="switchId"
|
||||
:checked="modelValue"
|
||||
:disabled="disabled"
|
||||
@change="handleChange"
|
||||
class="switch-checkbox"
|
||||
/>
|
||||
<label :for="switchId" class="switch-label" :class="[`switch-${size}`]"></label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { SwitchProps, SwitchEmits } from './types'
|
||||
|
||||
const props = withDefaults(defineProps<SwitchProps>(), {
|
||||
disabled: false,
|
||||
size: 'medium'
|
||||
})
|
||||
|
||||
const emit = defineEmits<SwitchEmits>()
|
||||
|
||||
// 生成唯一的 ID
|
||||
const switchId = computed(() => `switch-${Math.random().toString(36).substr(2, 9)}`)
|
||||
|
||||
const handleChange = (event: Event) => {
|
||||
if (props.disabled) return
|
||||
|
||||
const target = event.target as HTMLInputElement
|
||||
const newValue = target.checked
|
||||
emit('update:modelValue', newValue)
|
||||
emit('change', newValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.switch-wrapper {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.switch-wrapper.switch-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.switch-checkbox {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.switch-checkbox:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
display: inline-block;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #5a5a5a;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.15s ease;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.switch-label:hover {
|
||||
border-color: #6a6a6a;
|
||||
background: #353535;
|
||||
}
|
||||
|
||||
/* 选中状态的勾选标记 */
|
||||
.switch-checkbox:checked + .switch-label::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border: solid #ffffff;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
/* Small size */
|
||||
.switch-small {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.switch-checkbox:checked + .switch-small::after {
|
||||
left: 2px;
|
||||
top: -1px;
|
||||
width: 6px;
|
||||
height: 4px;
|
||||
border-width: 0 1px 1px 0;
|
||||
}
|
||||
|
||||
/* Medium size (default) */
|
||||
.switch-medium {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.switch-checkbox:checked + .switch-medium::after {
|
||||
left: 2px;
|
||||
top: 0px;
|
||||
width: 8px;
|
||||
height: 6px;
|
||||
border-width: 0 1.5px 1.5px 0;
|
||||
}
|
||||
|
||||
/* Large size */
|
||||
.switch-large {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.switch-checkbox:checked + .switch-large::after {
|
||||
left: 3px;
|
||||
top: 1px;
|
||||
width: 9px;
|
||||
height: 7px;
|
||||
border-width: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
/* Focus styles */
|
||||
.switch-checkbox:focus + .switch-label {
|
||||
outline: 2px solid #409eff;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
.switch-disabled .switch-checkbox,
|
||||
.switch-disabled .switch-label {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.switch-disabled .switch-label:hover {
|
||||
border-color: #5a5a5a;
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
.switch-disabled .switch-checkbox:checked + .switch-label:hover {
|
||||
background: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
</style>
|
||||
11
src/components/BasicControls/Switch/types.ts
Normal file
@ -0,0 +1,11 @@
|
||||
// Switch 组件类型定义
|
||||
export interface SwitchProps {
|
||||
modelValue: boolean
|
||||
disabled?: boolean
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
}
|
||||
|
||||
export interface SwitchEmits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'change', value: boolean): void
|
||||
}
|
||||
19
src/components/BasicControls/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
// 基础组件导出
|
||||
export { default as PropertyRow } from './PropertyRow/index.vue'
|
||||
export { default as NumberInput } from './Input/index.vue'
|
||||
export { default as TextInput } from './Input/TextInput.vue'
|
||||
export { default as ColorInput } from './Input/ColorInput.vue'
|
||||
export { default as Select } from './Select/index.vue'
|
||||
export { default as Switch } from './Switch/index.vue'
|
||||
export { default as Slider } from './Slider/index.vue'
|
||||
export { default as Field } from './Field/index.vue'
|
||||
|
||||
// 类型导出
|
||||
export type { Vector3, RangeValue, SelectOption } from './types'
|
||||
export type { PropertyRowProps } from './PropertyRow/types'
|
||||
export type { NumberInputProps, NumberInputEmits, TextInputProps, TextInputEmits } from './Input/types'
|
||||
export type { SelectProps, SelectEmits } from './Select/types'
|
||||
export type { SwitchProps, SwitchEmits } from './Switch/types'
|
||||
export type { SliderProps, SliderEmits } from './Slider/types'
|
||||
export type { FieldProps, FieldEmits, FieldValue, FieldError, AcceptedType } from './Field/types'
|
||||
|
||||
17
src/components/BasicControls/types.ts
Normal file
@ -0,0 +1,17 @@
|
||||
// 基础组件通用类型定义
|
||||
|
||||
export interface Vector3 {
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
}
|
||||
|
||||
export interface RangeValue {
|
||||
min: number
|
||||
max: number
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
value: string | number
|
||||
label: string
|
||||
}
|
||||
747
src/components/BottomPanel/Console/index.vue
Normal file
@ -0,0 +1,747 @@
|
||||
<template>
|
||||
<div class="console">
|
||||
<!-- 控制台头部 -->
|
||||
<div class="console-header">
|
||||
<div class="console-title">
|
||||
<h3>控制台</h3>
|
||||
<span class="log-count">{{ filteredLogs.length }} 条日志</span>
|
||||
</div>
|
||||
<div class="console-actions">
|
||||
<!-- 日志级别过滤 -->
|
||||
<div class="log-filters">
|
||||
<button
|
||||
v-for="level in logLevels"
|
||||
:key="level.type"
|
||||
:class="['filter-btn', level.type, { active: activeFilters.includes(level.type) }]"
|
||||
@click="toggleFilter(level.type)"
|
||||
:title="`${level.label} (${getLogCountByType(level.type)})`"
|
||||
>
|
||||
<span class="filter-icon">{{ level.icon }}</span>
|
||||
<span class="filter-count">{{ getLogCountByType(level.type) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<div class="control-buttons">
|
||||
<button @click="toggleAutoScroll" :class="{ active: autoScroll }" title="自动滚动">
|
||||
📜
|
||||
</button>
|
||||
<button @click="toggleTimestamp" :class="{ active: showTimestamp }" title="显示时间戳">
|
||||
🕐
|
||||
</button>
|
||||
<button @click="exportLogs" title="导出日志">
|
||||
💾
|
||||
</button>
|
||||
<button @click="clearLogs" title="清空日志">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="搜索日志内容..."
|
||||
class="search-input"
|
||||
>
|
||||
<button v-if="searchQuery" @click="clearSearch" class="clear-search">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 日志内容区域 -->
|
||||
<div
|
||||
ref="logContainer"
|
||||
class="log-container"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div class="log-list">
|
||||
<div
|
||||
v-for="(log, index) in filteredLogs"
|
||||
:key="log.id"
|
||||
:class="['log-item', log.type]"
|
||||
@click="selectLog(log)"
|
||||
:data-index="index"
|
||||
>
|
||||
<!-- 时间戳 -->
|
||||
<span v-if="showTimestamp" class="log-timestamp">
|
||||
{{ formatTime(log.timestamp) }}
|
||||
</span>
|
||||
|
||||
<!-- 日志级别图标 -->
|
||||
<span class="log-icon">
|
||||
{{ getLogIcon(log.type) }}
|
||||
</span>
|
||||
|
||||
<!-- 日志内容 -->
|
||||
<div class="log-content">
|
||||
<div class="log-message" v-html="highlightSearch(log.message)"></div>
|
||||
|
||||
<!-- 堆栈信息 -->
|
||||
<div v-if="log.stack && selectedLogId === log.id" class="log-stack">
|
||||
<pre>{{ log.stack }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- 额外数据 -->
|
||||
<div v-if="log.data && selectedLogId === log.id" class="log-data">
|
||||
<pre>{{ JSON.stringify(log.data, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志来源 -->
|
||||
<span v-if="log.source" class="log-source">
|
||||
{{ log.source }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="filteredLogs.length === 0" class="empty-state">
|
||||
<div v-if="logs.length === 0">
|
||||
<p>暂无日志</p>
|
||||
<button @click="addTestLogs">添加测试日志</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>没有匹配的日志</p>
|
||||
<button @click="clearFilters">清除过滤器</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部状态栏 -->
|
||||
<div class="console-footer">
|
||||
<div class="status-info">
|
||||
<span>总计: {{ logs.length }} 条</span>
|
||||
<span>显示: {{ filteredLogs.length }} 条</span>
|
||||
<span v-if="selectedLogId">已选择: 1 条</span>
|
||||
</div>
|
||||
<div class="scroll-info" v-if="!isAtBottom && filteredLogs.length > 0">
|
||||
<button @click="scrollToBottom" class="scroll-to-bottom">
|
||||
滚动到底部 ↓
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick, onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
// 日志类型定义
|
||||
interface LogEntry {
|
||||
id: string
|
||||
type: 'info' | 'warn' | 'error' | 'debug' | 'success'
|
||||
message: string
|
||||
timestamp: Date
|
||||
source?: string
|
||||
stack?: string
|
||||
data?: any
|
||||
}
|
||||
|
||||
// 日志级别配置
|
||||
const logLevels = [
|
||||
{ type: 'info', label: '信息', icon: 'ℹ️' },
|
||||
{ type: 'warn', label: '警告', icon: '⚠️' },
|
||||
{ type: 'error', label: '错误', icon: '❌' },
|
||||
{ type: 'debug', label: '调试', icon: '🐛' },
|
||||
{ type: 'success', label: '成功', icon: '✅' }
|
||||
] as const
|
||||
|
||||
// 响应式数据
|
||||
const logs = ref<LogEntry[]>([])
|
||||
const activeFilters = ref<string[]>(['info', 'warn', 'error', 'debug', 'success'])
|
||||
const searchQuery = ref('')
|
||||
const selectedLogId = ref<string>('')
|
||||
const autoScroll = ref(true)
|
||||
const showTimestamp = ref(true)
|
||||
const isAtBottom = ref(true)
|
||||
|
||||
// DOM 引用
|
||||
const logContainer = ref<HTMLElement>()
|
||||
|
||||
// 计算属性
|
||||
const filteredLogs = computed(() => {
|
||||
let filtered = logs.value.filter(log => activeFilters.value.includes(log.type))
|
||||
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
filtered = filtered.filter(log =>
|
||||
log.message.toLowerCase().includes(query) ||
|
||||
log.source?.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
// 工具函数
|
||||
function generateId(): string {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
||||
}
|
||||
|
||||
function formatTime(date: Date): string {
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
fractionalSecondDigits: 3
|
||||
})
|
||||
}
|
||||
|
||||
function getLogIcon(type: string): string {
|
||||
const level = logLevels.find(l => l.type === type)
|
||||
return level?.icon || 'ℹ️'
|
||||
}
|
||||
|
||||
function getLogCountByType(type: string): number {
|
||||
return logs.value.filter(log => log.type === type).length
|
||||
}
|
||||
|
||||
function highlightSearch(text: string): string {
|
||||
if (!searchQuery.value) return text
|
||||
|
||||
const regex = new RegExp(`(${searchQuery.value})`, 'gi')
|
||||
return text.replace(regex, '<mark>$1</mark>')
|
||||
}
|
||||
|
||||
// 日志操作
|
||||
function addLog(type: LogEntry['type'], message: string, options: Partial<LogEntry> = {}) {
|
||||
const log: LogEntry = {
|
||||
id: generateId(),
|
||||
type,
|
||||
message,
|
||||
timestamp: new Date(),
|
||||
...options
|
||||
}
|
||||
|
||||
logs.value.push(log)
|
||||
|
||||
// 自动滚动到底部
|
||||
if (autoScroll.value) {
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
logs.value = []
|
||||
selectedLogId.value = ''
|
||||
}
|
||||
|
||||
function addTestLogs() {
|
||||
const testLogs = [
|
||||
{ type: 'info' as const, message: '应用程序启动成功', source: 'App.vue' },
|
||||
{ type: 'success' as const, message: '场景加载完成', source: 'SceneManager.ts' },
|
||||
{ type: 'warn' as const, message: '纹理分辨率过大,可能影响性能', source: 'TextureLoader.ts' },
|
||||
{ type: 'error' as const, message: '无法加载模型文件: model.fbx', source: 'ModelLoader.ts', stack: 'Error: File not found\n at ModelLoader.load (ModelLoader.ts:45)\n at Scene.addModel (Scene.ts:123)' },
|
||||
{ type: 'debug' as const, message: '相机位置更新', source: 'Camera.ts', data: { position: { x: 0, y: 5, z: 10 }, rotation: { x: 0, y: 0, z: 0 } } },
|
||||
{ type: 'info' as const, message: '用户交互事件: 鼠标点击', source: 'InputManager.ts' },
|
||||
{ type: 'warn' as const, message: '内存使用率较高: 85%', source: 'PerformanceMonitor.ts' }
|
||||
]
|
||||
|
||||
testLogs.forEach(log => {
|
||||
addLog(log.type, log.message, { source: log.source, stack: log.stack, data: log.data })
|
||||
})
|
||||
}
|
||||
|
||||
// 过滤器操作
|
||||
function toggleFilter(type: string) {
|
||||
const index = activeFilters.value.indexOf(type)
|
||||
if (index > -1) {
|
||||
activeFilters.value.splice(index, 1)
|
||||
} else {
|
||||
activeFilters.value.push(type)
|
||||
}
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
activeFilters.value = ['info', 'warn', 'error', 'debug', 'success']
|
||||
searchQuery.value = ''
|
||||
}
|
||||
|
||||
// 搜索操作
|
||||
function clearSearch() {
|
||||
searchQuery.value = ''
|
||||
}
|
||||
|
||||
// 日志选择
|
||||
function selectLog(log: LogEntry) {
|
||||
selectedLogId.value = selectedLogId.value === log.id ? '' : log.id
|
||||
}
|
||||
|
||||
// 滚动控制
|
||||
function handleScroll() {
|
||||
if (!logContainer.value) return
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = logContainer.value
|
||||
isAtBottom.value = scrollTop + clientHeight >= scrollHeight - 10
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
if (!logContainer.value) return
|
||||
|
||||
logContainer.value.scrollTop = logContainer.value.scrollHeight
|
||||
isAtBottom.value = true
|
||||
}
|
||||
|
||||
function toggleAutoScroll() {
|
||||
autoScroll.value = !autoScroll.value
|
||||
if (autoScroll.value) {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTimestamp() {
|
||||
showTimestamp.value = !showTimestamp.value
|
||||
}
|
||||
|
||||
// 导出功能
|
||||
function exportLogs() {
|
||||
const logText = filteredLogs.value.map(log => {
|
||||
const timestamp = formatTime(log.timestamp)
|
||||
const level = log.type.toUpperCase()
|
||||
const source = log.source ? ` [${log.source}]` : ''
|
||||
return `[${timestamp}] ${level}${source}: ${log.message}`
|
||||
}).join('\n')
|
||||
|
||||
const blob = new Blob([logText], { type: 'text/plain' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `console-logs-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.txt`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
// 监听过滤器变化,自动滚动到底部
|
||||
watch(filteredLogs, () => {
|
||||
if (autoScroll.value) {
|
||||
nextTick(() => {
|
||||
scrollToBottom()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
addLog,
|
||||
clearLogs,
|
||||
info: (message: string, options?: Partial<LogEntry>) => addLog('info', message, options),
|
||||
warn: (message: string, options?: Partial<LogEntry>) => addLog('warn', message, options),
|
||||
error: (message: string, options?: Partial<LogEntry>) => addLog('error', message, options),
|
||||
debug: (message: string, options?: Partial<LogEntry>) => addLog('debug', message, options),
|
||||
success: (message: string, options?: Partial<LogEntry>) => addLog('success', message, options)
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 监听全局错误
|
||||
window.addEventListener('error', (event) => {
|
||||
addLog('error', event.message, {
|
||||
source: event.filename,
|
||||
stack: event.error?.stack
|
||||
})
|
||||
})
|
||||
|
||||
// 监听未处理的 Promise 拒绝
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
addLog('error', `未处理的 Promise 拒绝: ${event.reason}`, {
|
||||
source: 'Promise'
|
||||
})
|
||||
})
|
||||
|
||||
// 重写 console 方法
|
||||
const originalConsole = {
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
error: console.error,
|
||||
info: console.info,
|
||||
debug: console.debug
|
||||
}
|
||||
|
||||
console.log = (...args) => {
|
||||
originalConsole.log(...args)
|
||||
addLog('info', args.join(' '), { source: 'Console' })
|
||||
}
|
||||
|
||||
console.warn = (...args) => {
|
||||
originalConsole.warn(...args)
|
||||
addLog('warn', args.join(' '), { source: 'Console' })
|
||||
}
|
||||
|
||||
console.error = (...args) => {
|
||||
originalConsole.error(...args)
|
||||
addLog('error', args.join(' '), { source: 'Console' })
|
||||
}
|
||||
|
||||
console.info = (...args) => {
|
||||
originalConsole.info(...args)
|
||||
addLog('info', args.join(' '), { source: 'Console' })
|
||||
}
|
||||
|
||||
console.debug = (...args) => {
|
||||
originalConsole.debug(...args)
|
||||
addLog('debug', args.join(' '), { source: 'Console' })
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 恢复原始 console 方法
|
||||
// 这里可以根据需要决定是否恢复
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.console {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.console-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: #2d2d2d;
|
||||
border-bottom: 1px solid #3e3e3e;
|
||||
}
|
||||
|
||||
.console-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.console-title h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.log-count {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.console-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.log-filters {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: transparent;
|
||||
border: 1px solid #3e3e3e;
|
||||
color: #888;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
border-color: #007acc;
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: #007acc;
|
||||
border-color: #007acc;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter-btn.info.active {
|
||||
background: #007acc;
|
||||
}
|
||||
|
||||
.filter-btn.warn.active {
|
||||
background: #ff8c00;
|
||||
}
|
||||
|
||||
.filter-btn.error.active {
|
||||
background: #f14c4c;
|
||||
}
|
||||
|
||||
.filter-btn.debug.active {
|
||||
background: #9c27b0;
|
||||
}
|
||||
|
||||
.filter-btn.success.active {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.filter-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.filter-count {
|
||||
font-size: 10px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
min-width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.control-buttons button {
|
||||
background: transparent;
|
||||
border: 1px solid #3e3e3e;
|
||||
color: #888;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.control-buttons button:hover {
|
||||
border-color: #007acc;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.control-buttons button.active {
|
||||
background: #007acc;
|
||||
border-color: #007acc;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
padding: 8px 12px;
|
||||
background: #252526;
|
||||
border-bottom: 1px solid #3e3e3e;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
background: #3c3c3c;
|
||||
border: 1px solid #3e3e3e;
|
||||
color: #d4d4d4;
|
||||
padding: 6px 12px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: #007acc;
|
||||
}
|
||||
|
||||
.clear-search {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.clear-search:hover {
|
||||
background: #3e3e3e;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.log-item:hover {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
.log-item.info {
|
||||
border-left-color: #007acc;
|
||||
}
|
||||
|
||||
.log-item.warn {
|
||||
border-left-color: #ff8c00;
|
||||
}
|
||||
|
||||
.log-item.error {
|
||||
border-left-color: #f14c4c;
|
||||
}
|
||||
|
||||
.log-item.debug {
|
||||
border-left-color: #9c27b0;
|
||||
}
|
||||
|
||||
.log-item.success {
|
||||
border-left-color: #4caf50;
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
color: #888;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.log-icon {
|
||||
font-size: 12px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
word-break: break-word;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.log-message :deep(mark) {
|
||||
background: #ff8c00;
|
||||
color: #1e1e1e;
|
||||
padding: 1px 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.log-stack,
|
||||
.log-data {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background: #2d2d2d;
|
||||
border-radius: 3px;
|
||||
border-left: 3px solid #3e3e3e;
|
||||
}
|
||||
|
||||
.log-stack pre,
|
||||
.log-data pre {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.log-source {
|
||||
color: #888;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.empty-state button {
|
||||
background: #007acc;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.empty-state button:hover {
|
||||
background: #005a9e;
|
||||
}
|
||||
|
||||
.console-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 12px;
|
||||
background: #2d2d2d;
|
||||
border-top: 1px solid #3e3e3e;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.status-info {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.scroll-to-bottom {
|
||||
background: #007acc;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.scroll-to-bottom:hover {
|
||||
background: #005a9e;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.log-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.log-container::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.log-container::-webkit-scrollbar-thumb {
|
||||
background: #3e3e3e;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.log-container::-webkit-scrollbar-thumb:hover {
|
||||
background: #4e4e4e;
|
||||
}
|
||||
</style>
|
||||
959
src/components/BottomPanel/FileSystem/index copy.vue
Normal file
@ -0,0 +1,959 @@
|
||||
<template>
|
||||
<div class="file-system">
|
||||
<!-- 左侧文件夹树 -->
|
||||
<div
|
||||
class="file-tree-panel"
|
||||
:style="{ width: leftPanelWidth + 'px' }"
|
||||
>
|
||||
|
||||
|
||||
<div class="tree-content">
|
||||
<el-tree
|
||||
:data="folderTree"
|
||||
:props="treeProps"
|
||||
:expand-on-click-node="false"
|
||||
node-key="id"
|
||||
:current-node-key="selectedFolderId"
|
||||
@node-click="handleFolderClick"
|
||||
@node-expand="handleFolderExpand"
|
||||
@node-collapse="handleFolderCollapse"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<span class="tree-node-content">
|
||||
<i class="tree-node-icon">📁</i>
|
||||
<span class="tree-node-label">{{ node.label }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
|
||||
<div v-if="folderTree.length === 0" class="empty-state">
|
||||
<p>暂无文件夹</p>
|
||||
<button @click="createFolder">创建文件夹</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 拖拽分隔条 -->
|
||||
<div
|
||||
class="resize-handle"
|
||||
@mousedown="startResize"
|
||||
></div>
|
||||
|
||||
<!-- 右侧文件列表 -->
|
||||
<div class="file-list-panel">
|
||||
<div class="list-content" :class="viewMode">
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="breadcrumb">
|
||||
<span @click="navigateToRoot" class="breadcrumb-item">根目录</span>
|
||||
<span
|
||||
v-for="(folder, index) in currentPath"
|
||||
:key="folder.id"
|
||||
@click="navigateToFolder(folder, index)"
|
||||
class="breadcrumb-item"
|
||||
>
|
||||
/ {{ folder.name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 文件网格视图 -->
|
||||
<div class="file-grid" v-if="viewMode === 'grid'">
|
||||
<FileNode
|
||||
v-for="node in currentFolderFiles"
|
||||
:key="node.id"
|
||||
:node="node"
|
||||
:selected-id="selectedFileId"
|
||||
:show-size="true"
|
||||
@click="handleFileClick"
|
||||
@double-click="handleFileDoubleClick"
|
||||
@rename="handleFileRename"
|
||||
@drag-start="handleDragStart"
|
||||
@drag-end="handleDragEnd"
|
||||
@drag-over="handleDragOver"
|
||||
@drag-leave="handleDragLeave"
|
||||
@drop="handleDrop"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 文件列表视图 -->
|
||||
<div class="file-list" v-else>
|
||||
<div class="list-header">
|
||||
<div class="col-name">名称</div>
|
||||
<div class="col-type">类型</div>
|
||||
<div class="col-size">大小</div>
|
||||
<div class="col-date">修改时间</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="node in currentFolderFiles"
|
||||
:key="node.id"
|
||||
:class="['file-row', node.type.toLowerCase(), { selected: selectedFileId === node.id }]"
|
||||
@click="handleFileClick(node)"
|
||||
@dblclick="handleFileDoubleClick(node)"
|
||||
:draggable="true"
|
||||
@dragstart="handleDragStart(node, $event)"
|
||||
@dragend="handleDragEnd(node, $event)"
|
||||
@dragover="handleDragOver(node, $event)"
|
||||
@dragleave="handleDragLeave(node, $event)"
|
||||
@drop="handleDrop(node, dragState.dragNode!, 'inside', $event)"
|
||||
>
|
||||
<div class="col-name">
|
||||
<span class="file-icon">{{ getFileIcon(node.type) }}</span>
|
||||
<span class="file-name" :title="node.name">{{ node.name }}</span>
|
||||
</div>
|
||||
<div class="col-type">{{ getFileTypeLabel(node.type) }}</div>
|
||||
<div class="col-size">{{ node.size !== undefined ? formatFileSize(node.size) : '-' }}</div>
|
||||
<div class="col-date">{{ node.lastModified ? formatDate(node.lastModified) : '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="currentFolderFiles.length === 0" class="empty-state">
|
||||
<p>此文件夹为空</p>
|
||||
<div class="empty-actions">
|
||||
<button @click="createFile">创建文件</button>
|
||||
<button @click="uploadFile">上传文件</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { FileNode as FileNodeType, FileNodeType as NodeType, DragState, FILE_ICONS } from '../../BasicControls/FileNode/types'
|
||||
import { generateId, formatFileSize, formatDate } from '../../../utils/Tools'
|
||||
import FileNode from '../../BasicControls/FileNode/index.vue'
|
||||
|
||||
// 响应式数据
|
||||
const leftPanelWidth = ref(250)
|
||||
const minPanelWidth = 150
|
||||
const maxPanelWidth = 500
|
||||
|
||||
const viewMode = ref<'list' | 'grid'>('grid')
|
||||
const selectedFolderId = ref<string>('')
|
||||
const selectedFileId = ref<string>('')
|
||||
const currentPath = ref<FileNodeType[]>([])
|
||||
|
||||
// Element Plus 树组件配置
|
||||
const treeProps = {
|
||||
children: 'children',
|
||||
label: 'name'
|
||||
}
|
||||
|
||||
// 拖拽状态
|
||||
const dragState = ref<DragState>({
|
||||
isDragging: false,
|
||||
dragNode: null,
|
||||
dragOverNode: null,
|
||||
dropPosition: null
|
||||
})
|
||||
|
||||
// 模拟文件数据
|
||||
const fileTree = ref<FileNodeType[]>([
|
||||
{
|
||||
id: 'root',
|
||||
name: '项目文件',
|
||||
type: NodeType.Folder,
|
||||
path: '/',
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: 'assets',
|
||||
name: 'Assets',
|
||||
type: NodeType.Folder,
|
||||
path: '/Assets',
|
||||
isExpanded: false,
|
||||
children: [
|
||||
{
|
||||
id: 'models',
|
||||
name: 'Models',
|
||||
type: NodeType.Folder,
|
||||
path: '/Assets/Models',
|
||||
children: [
|
||||
{
|
||||
id: 'model1',
|
||||
name: 'character.fbx',
|
||||
type: NodeType.Model,
|
||||
path: '/Assets/Models/character.fbx',
|
||||
size: 2048576,
|
||||
lastModified: new Date('2024-01-15')
|
||||
},
|
||||
{
|
||||
id: 'model2',
|
||||
name: 'environment.glb',
|
||||
type: NodeType.Model,
|
||||
path: '/Assets/Models/environment.glb',
|
||||
size: 5242880,
|
||||
lastModified: new Date('2024-01-18')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'textures',
|
||||
name: 'Textures',
|
||||
type: NodeType.Folder,
|
||||
path: '/Assets/Textures',
|
||||
children: [
|
||||
{
|
||||
id: 'texture1',
|
||||
name: 'wall_diffuse.png',
|
||||
type: NodeType.Texture,
|
||||
path: '/Assets/Textures/wall_diffuse.png',
|
||||
size: 1024000,
|
||||
lastModified: new Date('2024-01-10')
|
||||
},
|
||||
{
|
||||
id: 'texture2',
|
||||
name: 'floor_normal.jpg',
|
||||
type: NodeType.Image,
|
||||
path: '/Assets/Textures/floor_normal.jpg',
|
||||
size: 512000,
|
||||
lastModified: new Date('2024-01-12')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'materials',
|
||||
name: 'Materials',
|
||||
type: NodeType.Folder,
|
||||
path: '/Assets/Materials',
|
||||
children: [
|
||||
{
|
||||
id: 'material1',
|
||||
name: 'wood.material',
|
||||
type: NodeType.Material,
|
||||
path: '/Assets/Materials/wood.material',
|
||||
size: 2048,
|
||||
lastModified: new Date('2024-01-14')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'audio',
|
||||
name: 'Audio',
|
||||
type: NodeType.Folder,
|
||||
path: '/Assets/Audio',
|
||||
children: [
|
||||
{
|
||||
id: 'audio1',
|
||||
name: 'background.mp3',
|
||||
type: NodeType.Audio,
|
||||
path: '/Assets/Audio/background.mp3',
|
||||
size: 3145728,
|
||||
lastModified: new Date('2024-01-16')
|
||||
},
|
||||
{
|
||||
id: 'audio2',
|
||||
name: 'effect.wav',
|
||||
type: NodeType.Audio,
|
||||
path: '/Assets/Audio/effect.wav',
|
||||
size: 1048576,
|
||||
lastModified: new Date('2024-01-17')
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'scripts',
|
||||
name: 'Scripts',
|
||||
type: NodeType.Folder,
|
||||
path: '/Scripts',
|
||||
children: [
|
||||
{
|
||||
id: 'script1',
|
||||
name: 'main.js',
|
||||
type: NodeType.Script,
|
||||
path: '/Scripts/main.js',
|
||||
size: 4096,
|
||||
lastModified: new Date('2024-01-20')
|
||||
},
|
||||
{
|
||||
id: 'script2',
|
||||
name: 'utils.ts',
|
||||
type: NodeType.Script,
|
||||
path: '/Scripts/utils.ts',
|
||||
size: 2048,
|
||||
lastModified: new Date('2024-01-19')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'scenes',
|
||||
name: 'Scenes',
|
||||
type: NodeType.Folder,
|
||||
path: '/Scenes',
|
||||
children: [
|
||||
{
|
||||
id: 'scene1',
|
||||
name: 'main.scene',
|
||||
type: NodeType.Scene,
|
||||
path: '/Scenes/main.scene',
|
||||
size: 8192,
|
||||
lastModified: new Date('2024-01-21')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'docs',
|
||||
name: 'Documentation',
|
||||
type: NodeType.Folder,
|
||||
path: '/Documentation',
|
||||
children: [
|
||||
{
|
||||
id: 'readme',
|
||||
name: 'README.md',
|
||||
type: NodeType.Text,
|
||||
path: '/Documentation/README.md',
|
||||
size: 1024,
|
||||
lastModified: new Date('2024-01-22')
|
||||
},
|
||||
{
|
||||
id: 'guide',
|
||||
name: 'user-guide.txt',
|
||||
type: NodeType.Text,
|
||||
path: '/Documentation/user-guide.txt',
|
||||
size: 2048,
|
||||
lastModified: new Date('2024-01-23')
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const folderTree = computed(() => {
|
||||
return buildFolderTree(fileTree.value)
|
||||
})
|
||||
|
||||
|
||||
const currentFolderFiles = computed(() => {
|
||||
let currentFolder = fileTree.value[0] // 根目录
|
||||
|
||||
for (const pathItem of currentPath.value) {
|
||||
const found = currentFolder.children?.find(child => child.id === pathItem.id)
|
||||
if (found) {
|
||||
currentFolder = found
|
||||
}
|
||||
}
|
||||
|
||||
return currentFolder.children || []
|
||||
})
|
||||
|
||||
// 工具函数
|
||||
function buildFolderTree(nodes: FileNodeType[]): FileNodeType[] {
|
||||
return nodes.map(node => {
|
||||
if (node.type === NodeType.Folder) {
|
||||
const folderNode: FileNodeType = {
|
||||
...node,
|
||||
children: node.children ? buildFolderTree(node.children.filter(child => child.type === NodeType.Folder)) : []
|
||||
}
|
||||
return folderNode
|
||||
}
|
||||
return node
|
||||
}).filter(node => node.type === NodeType.Folder)
|
||||
}
|
||||
|
||||
function findNodeById(nodes: FileNodeType[], id: string): FileNodeType | null {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) {
|
||||
return node
|
||||
}
|
||||
if (node.children) {
|
||||
const found = findNodeById(node.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getFileIcon(type: NodeType): string {
|
||||
return FILE_ICONS[type] || '📄'
|
||||
}
|
||||
|
||||
function getFileTypeLabel(type: NodeType): string {
|
||||
const typeLabels: Record<NodeType, string> = {
|
||||
[NodeType.Text]: '文本',
|
||||
[NodeType.Folder]: '文件夹',
|
||||
[NodeType.Model]: '模型',
|
||||
[NodeType.Audio]: '音频',
|
||||
[NodeType.Video]: '视频',
|
||||
[NodeType.Material]: '材质',
|
||||
[NodeType.Texture]: '纹理',
|
||||
[NodeType.Image]: '图片',
|
||||
[NodeType.Script]: '脚本',
|
||||
[NodeType.Scene]: '场景'
|
||||
}
|
||||
return typeLabels[type] || '未知'
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
const handleFolderClick = (data: FileNodeType) => {
|
||||
selectedFolderId.value = data.id
|
||||
// 导航到选中的文件夹
|
||||
const pathToNode = getPathToNode(fileTree.value, data.id)
|
||||
currentPath.value = pathToNode.slice(1) // 移除根目录
|
||||
}
|
||||
|
||||
const handleFolderExpand = (data: FileNodeType) => {
|
||||
const targetNode = findNodeById(fileTree.value, data.id)
|
||||
if (targetNode) {
|
||||
targetNode.isExpanded = true
|
||||
}
|
||||
}
|
||||
|
||||
const handleFolderCollapse = (data: FileNodeType) => {
|
||||
const targetNode = findNodeById(fileTree.value, data.id)
|
||||
if (targetNode) {
|
||||
targetNode.isExpanded = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileClick = (node: FileNodeType) => {
|
||||
selectedFileId.value = node.id
|
||||
}
|
||||
|
||||
const handleFileRename = (node: FileNodeType, newName: string) => {
|
||||
const targetNode = findNodeById(fileTree.value, node.id)
|
||||
if (targetNode) {
|
||||
targetNode.name = newName
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileDoubleClick = (node: FileNodeType) => {
|
||||
if (node.type === NodeType.Folder) {
|
||||
// 进入文件夹
|
||||
currentPath.value.push(node)
|
||||
selectedFolderId.value = node.id
|
||||
selectedFileId.value = ''
|
||||
} else {
|
||||
// 打开文件
|
||||
console.log('打开文件:', node.name)
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽处理
|
||||
const handleDragStart = (node: FileNodeType, event: DragEvent) => {
|
||||
dragState.value.isDragging = true
|
||||
dragState.value.dragNode = node
|
||||
node.isDragging = true
|
||||
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
event.dataTransfer.setData('text/plain', node.id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragEnd = (node: FileNodeType, event: DragEvent) => {
|
||||
dragState.value.isDragging = false
|
||||
dragState.value.dragNode = null
|
||||
dragState.value.dragOverNode = null
|
||||
dragState.value.dropPosition = null
|
||||
node.isDragging = false
|
||||
}
|
||||
|
||||
const handleDragOver = (node: FileNodeType, event: DragEvent) => {
|
||||
if (dragState.value.dragNode && dragState.value.dragNode.id !== node.id) {
|
||||
event.preventDefault()
|
||||
dragState.value.dragOverNode = node
|
||||
dragState.value.dropPosition = 'inside'
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragLeave = (node: FileNodeType, event: DragEvent) => {
|
||||
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
|
||||
const x = event.clientX
|
||||
const y = event.clientY
|
||||
|
||||
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
|
||||
if (dragState.value.dragOverNode?.id === node.id) {
|
||||
dragState.value.dragOverNode = null
|
||||
dragState.value.dropPosition = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrop = (targetNode: FileNodeType, dragNode: FileNodeType, position: string, event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
console.log('拖拽操作:', {
|
||||
target: targetNode.name,
|
||||
drag: dragNode.name,
|
||||
position
|
||||
})
|
||||
|
||||
// 这里实现具体的拖拽逻辑
|
||||
}
|
||||
|
||||
// 导航功能
|
||||
const navigateToRoot = () => {
|
||||
currentPath.value = []
|
||||
selectedFolderId.value = fileTree.value[0].id
|
||||
selectedFileId.value = ''
|
||||
}
|
||||
|
||||
const navigateToFolder = (folder: FileNodeType, index: number) => {
|
||||
currentPath.value = currentPath.value.slice(0, index + 1)
|
||||
selectedFolderId.value = folder.id
|
||||
selectedFileId.value = ''
|
||||
}
|
||||
|
||||
function getPathToNode(nodes: FileNodeType[], targetId: string, currentPath: FileNodeType[] = []): FileNodeType[] {
|
||||
for (const node of nodes) {
|
||||
const newPath = [...currentPath, node]
|
||||
if (node.id === targetId) {
|
||||
return newPath
|
||||
}
|
||||
if (node.children) {
|
||||
const found = getPathToNode(node.children, targetId, newPath)
|
||||
if (found.length > 0) return found
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// 操作功能
|
||||
const toggleViewMode = () => {
|
||||
viewMode.value = viewMode.value === 'list' ? 'grid' : 'list'
|
||||
}
|
||||
|
||||
const createFolder = () => {
|
||||
const newFolder: FileNodeType = {
|
||||
id: generateId(),
|
||||
name: '新建文件夹',
|
||||
type: NodeType.Folder,
|
||||
path: '/新建文件夹',
|
||||
children: []
|
||||
}
|
||||
|
||||
// 添加到当前位置
|
||||
if (currentPath.value.length === 0) {
|
||||
fileTree.value[0].children = fileTree.value[0].children || []
|
||||
fileTree.value[0].children.push(newFolder)
|
||||
} else {
|
||||
const currentFolder = findNodeById(fileTree.value, currentPath.value[currentPath.value.length - 1].id)
|
||||
if (currentFolder) {
|
||||
currentFolder.children = currentFolder.children || []
|
||||
currentFolder.children.push(newFolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createFile = () => {
|
||||
const newFile: FileNodeType = {
|
||||
id: generateId(),
|
||||
name: '新建文件.txt',
|
||||
type: NodeType.Text,
|
||||
path: '/新建文件.txt',
|
||||
size: 0,
|
||||
lastModified: new Date()
|
||||
}
|
||||
|
||||
// 添加到当前文件夹
|
||||
if (currentPath.value.length === 0) {
|
||||
fileTree.value[0].children = fileTree.value[0].children || []
|
||||
fileTree.value[0].children.push(newFile)
|
||||
} else {
|
||||
const currentFolder = findNodeById(fileTree.value, currentPath.value[currentPath.value.length - 1].id)
|
||||
if (currentFolder) {
|
||||
currentFolder.children = currentFolder.children || []
|
||||
currentFolder.children.push(newFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uploadFile = () => {
|
||||
// 创建文件输入元素
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.multiple = true
|
||||
input.onchange = (e) => {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (files) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i]
|
||||
console.log('上传文件:', file.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
const refreshTree = () => {
|
||||
console.log('刷新文件树')
|
||||
}
|
||||
|
||||
// 拖拽调整面板大小
|
||||
let isResizing = false
|
||||
|
||||
const startResize = (event: MouseEvent) => {
|
||||
isResizing = true
|
||||
document.addEventListener('mousemove', handleResize)
|
||||
document.addEventListener('mouseup', stopResize)
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
const handleResize = (event: MouseEvent) => {
|
||||
if (!isResizing) return
|
||||
|
||||
const container = document.querySelector('.file-system') as HTMLElement
|
||||
if (!container) return
|
||||
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const newWidth = event.clientX - containerRect.left
|
||||
|
||||
if (newWidth >= minPanelWidth && newWidth <= maxPanelWidth) {
|
||||
leftPanelWidth.value = newWidth
|
||||
}
|
||||
}
|
||||
|
||||
const stopResize = () => {
|
||||
isResizing = false
|
||||
document.removeEventListener('mousemove', handleResize)
|
||||
document.removeEventListener('mouseup', stopResize)
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 初始化选中根目录
|
||||
selectedFolderId.value = fileTree.value[0].id
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', handleResize)
|
||||
document.removeEventListener('mouseup', stopResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
.file-system {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background: #2d2d2d;
|
||||
color: #cccccc;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.file-tree-panel {
|
||||
background: #393939;
|
||||
border-right: 1px solid #4a4a4a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.tree-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
|
||||
.tree-node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tree-node-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tree-node-label {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.resize-handle {
|
||||
width: 4px;
|
||||
background: #4a4a4a;
|
||||
cursor: col-resize;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.resize-handle:hover {
|
||||
background: #409eff;
|
||||
}
|
||||
|
||||
.file-list-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #353535;
|
||||
border-bottom: 1px solid #4a4a4a;
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
cursor: pointer;
|
||||
color: #409eff;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
color: #66b1ff;
|
||||
}
|
||||
|
||||
/* 网格视图样式 */
|
||||
.file-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||
gap: 1px;
|
||||
padding: 8px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* 文件列表视图样式 */
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
padding: 8px 12px;
|
||||
background: #353535;
|
||||
border-bottom: 1px solid #4a4a4a;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.file-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
border-radius: 3px;
|
||||
margin: 1px 4px;
|
||||
}
|
||||
|
||||
.file-row:hover {
|
||||
background: #404040;
|
||||
}
|
||||
|
||||
.file-row.selected {
|
||||
background: #2d4a6b;
|
||||
}
|
||||
|
||||
.col-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.col-type {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.col-size {
|
||||
width: 80px;
|
||||
text-align: right;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.col-date {
|
||||
width: 120px;
|
||||
text-align: right;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.empty-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.empty-actions button {
|
||||
background: #409eff;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.empty-actions button:hover {
|
||||
background: #66b1ff;
|
||||
}
|
||||
|
||||
/* 文件类型特定样式 */
|
||||
.file-item.folder .file-item-name,
|
||||
.file-row.folder .file-name {
|
||||
color: #e6a23c;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-item.script .file-item-name,
|
||||
.file-row.script .file-name {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.file-item.model .file-item-name,
|
||||
.file-row.model .file-name {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.file-item.image .file-item-name,
|
||||
.file-item.texture .file-item-name,
|
||||
.file-row.image .file-name,
|
||||
.file-row.texture .file-name {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.file-item.material .file-item-name,
|
||||
.file-row.material .file-name {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.tree-content::-webkit-scrollbar,
|
||||
.list-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.tree-content::-webkit-scrollbar-track,
|
||||
.list-content::-webkit-scrollbar-track {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
.tree-content::-webkit-scrollbar-thumb,
|
||||
.list-content::-webkit-scrollbar-thumb {
|
||||
background: #4a4a4a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tree-content::-webkit-scrollbar-thumb:hover,
|
||||
.list-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #5a5a5a;
|
||||
}
|
||||
|
||||
/* Element Plus 树组件样式覆盖 */
|
||||
/* :deep(.el-tree) {
|
||||
background: transparent;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.tree-content :deep(.el-tree-node__content) {
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
margin: 2px 0;
|
||||
padding: 4px 8px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.tree-content :deep(.el-tree-node__content:hover) {
|
||||
background: #404040;
|
||||
}
|
||||
|
||||
.tree-content :deep(.el-tree-node.is-current > .el-tree-node__content) {
|
||||
background: #409eff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tree-content :deep(.el-tree-node__expand-icon) {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tree-content :deep(.el-tree-node__expand-icon.is-leaf) {
|
||||
color: transparent;
|
||||
}
|
||||
*/
|
||||
|
||||
.tree-node-label{
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.el-tree {
|
||||
background: #1c00be00;
|
||||
}
|
||||
|
||||
|
||||
/* 设置节点的默认文字颜色 */
|
||||
::v-deep .el-tree-node__label {
|
||||
color: #ffffff;
|
||||
/* 自定义你想要的颜色 */
|
||||
}
|
||||
|
||||
/* 设置鼠标悬停时的背景颜色 */
|
||||
::v-deep .el-tree-node:hover>.el-tree-node__content {
|
||||
background-color: #f0f9eb00;
|
||||
/* 自定义悬停背景色 */
|
||||
}
|
||||
|
||||
/* 设置鼠标悬停时的文字颜色 */
|
||||
::v-deep .el-tree-node:hover .el-tree-node__label {
|
||||
color: #f8f8f8;
|
||||
/* 自定义悬停文字颜色 */
|
||||
}
|
||||
|
||||
::v-deep .el-tree-node__content {
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
/* 鼠标离开后恢复的背景色 */
|
||||
color: #333;
|
||||
/* 默认文字颜色 */
|
||||
}
|
||||
|
||||
::v-deep .el-tree-node.is-current>.el-tree-node__content {
|
||||
background-color: #ffffff28;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
922
src/components/BottomPanel/FileSystem/index.vue
Normal file
@ -0,0 +1,922 @@
|
||||
<template>
|
||||
<div class="file-system">
|
||||
<!-- 左侧文件夹树 -->
|
||||
<div class="file-tree-panel" :style="{ width: leftPanelWidth + 'px' }">
|
||||
|
||||
|
||||
<div class="tree-content">
|
||||
<el-tree :data="folderTree" :props="treeProps" :expand-on-click-node="false" node-key="id"
|
||||
:current-node-key="selectedFolderId" @node-click="handleFolderClick" @node-expand="handleFolderExpand"
|
||||
@node-collapse="handleFolderCollapse">
|
||||
<template #default="{ node, data }">
|
||||
<span class="tree-node-content">
|
||||
<i class="tree-node-icon">📁</i>
|
||||
<span class="tree-node-label">{{ node.label }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
|
||||
<div v-if="folderTree.length === 0" class="empty-state">
|
||||
<p>暂无文件夹</p>
|
||||
<button @click="createFolder">创建文件夹</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 拖拽分隔条 -->
|
||||
<div class="resize-handle" @mousedown="startResize"></div>
|
||||
|
||||
<!-- 右侧文件列表 -->
|
||||
<div class="file-list-panel">
|
||||
<div class="list-content" :class="viewMode">
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="breadcrumb">
|
||||
<span @click="navigateToRoot" class="breadcrumb-item">根目录</span>
|
||||
<span v-for="(folder, index) in currentPath" :key="folder.id" @click="navigateToFolder(folder, index)"
|
||||
class="breadcrumb-item">
|
||||
/ {{ folder.name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 文件网格视图 -->
|
||||
<div class="file-grid" v-if="viewMode === 'grid'">
|
||||
<FileNode v-for="node in currentFolderFiles" :key="node.id" :node="node" :selected-id="selectedFileId"
|
||||
:show-size="true" @click="handleFileClick" @double-click="handleFileDoubleClick" @rename="handleFileRename"
|
||||
@drag-start="handleDragStart" @drag-end="handleDragEnd" @drag-over="handleDragOver"
|
||||
@drag-leave="handleDragLeave" @drop="handleDrop" />
|
||||
</div>
|
||||
|
||||
<!-- 文件列表视图 -->
|
||||
<div class="file-list" v-else>
|
||||
<div class="list-header">
|
||||
<div class="col-name">名称</div>
|
||||
<div class="col-type">类型</div>
|
||||
<div class="col-size">大小</div>
|
||||
<div class="col-date">修改时间</div>
|
||||
</div>
|
||||
<div v-for="node in currentFolderFiles" :key="node.id"
|
||||
:class="['file-row', node.type.toLowerCase(), { selected: selectedFileId === node.id }]"
|
||||
@click="handleFileClick(node)" @dblclick="handleFileDoubleClick(node)" :draggable="true"
|
||||
@dragstart="handleDragStart(node, $event)" @dragend="handleDragEnd(node, $event)"
|
||||
@dragover="handleDragOver(node, $event)" @dragleave="handleDragLeave(node, $event)"
|
||||
@drop="handleDrop(node, dragState.dragNode!, 'inside', $event)">
|
||||
<div class="col-name">
|
||||
<span class="file-icon">{{ getFileIcon(node.type) }}</span>
|
||||
<span class="file-name" :title="node.name">{{ node.name }}</span>
|
||||
</div>
|
||||
<div class="col-type">{{ getFileTypeLabel(node.type) }}</div>
|
||||
<div class="col-size">{{ node.size !== undefined ? formatFileSize(node.size) : '-' }}</div>
|
||||
<div class="col-date">{{ node.lastModified ? formatDate(node.lastModified) : '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="currentFolderFiles.length === 0" class="empty-state">
|
||||
<p>此文件夹为空</p>
|
||||
<div class="empty-actions">
|
||||
<button @click="createFile">创建文件</button>
|
||||
<button @click="uploadFile">上传文件</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { FileNode as FileNodeType, FileNodeType as NodeType, DragState, FILE_ICONS } from '../../BasicControls/FileNode/types'
|
||||
import { generateId, formatFileSize, formatDate } from '../../../utils/Tools'
|
||||
import FileNode from '../../BasicControls/FileNode/index.vue'
|
||||
import { userDragStore } from 'stores/Filed'
|
||||
import { useCursorStore } from 'stores/Cursor'
|
||||
const dragStore = userDragStore()
|
||||
const cursorStore = useCursorStore()
|
||||
// 响应式数据
|
||||
const leftPanelWidth = ref(250)
|
||||
const minPanelWidth = 150
|
||||
const maxPanelWidth = 500
|
||||
|
||||
const viewMode = ref<'list' | 'grid'>('grid')
|
||||
const selectedFolderId = ref<string>('')
|
||||
const selectedFileId = ref<string>('')
|
||||
const currentPath = ref<FileNodeType[]>([])
|
||||
|
||||
// Element Plus 树组件配置
|
||||
const treeProps = {
|
||||
children: 'children',
|
||||
label: 'name'
|
||||
}
|
||||
|
||||
// 拖拽状态
|
||||
const dragState = ref<DragState>({
|
||||
isDragging: false,
|
||||
dragNode: null,
|
||||
dragOverNode: null,
|
||||
dropPosition: null
|
||||
})
|
||||
|
||||
// 模拟文件数据
|
||||
const fileTree = ref<FileNodeType[]>([
|
||||
{
|
||||
id: 'root',
|
||||
name: '项目文件',
|
||||
type: NodeType.Folder,
|
||||
path: '/',
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: 'assets',
|
||||
name: 'Assets',
|
||||
type: NodeType.Folder,
|
||||
path: '/Assets',
|
||||
isExpanded: false,
|
||||
children: [
|
||||
{
|
||||
id: 'models',
|
||||
name: 'Models',
|
||||
type: NodeType.Folder,
|
||||
path: '/Assets/Models',
|
||||
children: [
|
||||
{
|
||||
id: 'model1',
|
||||
name: 'character.fbx',
|
||||
type: NodeType.Model,
|
||||
path: '/Assets/Models/character.fbx',
|
||||
size: 2048576,
|
||||
lastModified: new Date('2024-01-15')
|
||||
},
|
||||
{
|
||||
id: 'model2',
|
||||
name: 'environment.glb',
|
||||
type: NodeType.Model,
|
||||
path: '/Assets/Models/environment.glb',
|
||||
size: 5242880,
|
||||
lastModified: new Date('2024-01-18')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'textures',
|
||||
name: 'Textures',
|
||||
type: NodeType.Folder,
|
||||
path: '/Assets/Textures',
|
||||
children: [
|
||||
{
|
||||
id: 'texture1',
|
||||
name: 'wall_diffuse.png',
|
||||
type: NodeType.Texture,
|
||||
path: '/Assets/Textures/wall_diffuse.png',
|
||||
size: 1024000,
|
||||
lastModified: new Date('2024-01-10')
|
||||
},
|
||||
{
|
||||
id: 'texture2',
|
||||
name: 'floor_normal.jpg',
|
||||
type: NodeType.Image,
|
||||
path: '/Assets/Textures/floor_normal.jpg',
|
||||
size: 512000,
|
||||
lastModified: new Date('2024-01-12')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'materials',
|
||||
name: 'Materials',
|
||||
type: NodeType.Folder,
|
||||
path: '/Assets/Materials',
|
||||
children: [
|
||||
{
|
||||
id: 'material1',
|
||||
name: 'wood.material',
|
||||
type: NodeType.Material,
|
||||
path: '/Assets/Materials/wood.material',
|
||||
size: 2048,
|
||||
lastModified: new Date('2024-01-14')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'audio',
|
||||
name: 'Audio',
|
||||
type: NodeType.Folder,
|
||||
path: '/Assets/Audio',
|
||||
children: [
|
||||
{
|
||||
id: 'audio1',
|
||||
name: 'background.mp3',
|
||||
type: NodeType.Audio,
|
||||
path: '/Assets/Audio/background.mp3',
|
||||
size: 3145728,
|
||||
lastModified: new Date('2024-01-16')
|
||||
},
|
||||
{
|
||||
id: 'audio2',
|
||||
name: 'effect.wav',
|
||||
type: NodeType.Audio,
|
||||
path: '/Assets/Audio/effect.wav',
|
||||
size: 1048576,
|
||||
lastModified: new Date('2024-01-17')
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'scripts',
|
||||
name: 'Scripts',
|
||||
type: NodeType.Folder,
|
||||
path: '/Scripts',
|
||||
children: [
|
||||
{
|
||||
id: 'script1',
|
||||
name: 'main.js',
|
||||
type: NodeType.Script,
|
||||
path: '/Scripts/main.js',
|
||||
size: 4096,
|
||||
lastModified: new Date('2024-01-20')
|
||||
},
|
||||
{
|
||||
id: 'script2',
|
||||
name: 'utils.ts',
|
||||
type: NodeType.Script,
|
||||
path: '/Scripts/utils.ts',
|
||||
size: 2048,
|
||||
lastModified: new Date('2024-01-19')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'scenes',
|
||||
name: 'Scenes',
|
||||
type: NodeType.Folder,
|
||||
path: '/Scenes',
|
||||
children: [
|
||||
{
|
||||
id: 'scene1',
|
||||
name: 'main.scene',
|
||||
type: NodeType.Scene,
|
||||
path: '/Scenes/main.scene',
|
||||
size: 8192,
|
||||
lastModified: new Date('2024-01-21')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'docs',
|
||||
name: 'Documentation',
|
||||
type: NodeType.Folder,
|
||||
path: '/Documentation',
|
||||
children: [
|
||||
{
|
||||
id: 'readme',
|
||||
name: 'README.md',
|
||||
type: NodeType.Text,
|
||||
path: '/Documentation/README.md',
|
||||
size: 1024,
|
||||
lastModified: new Date('2024-01-22')
|
||||
},
|
||||
{
|
||||
id: 'guide',
|
||||
name: 'user-guide.txt',
|
||||
type: NodeType.Text,
|
||||
path: '/Documentation/user-guide.txt',
|
||||
size: 2048,
|
||||
lastModified: new Date('2024-01-23')
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const folderTree = computed(() => {
|
||||
return buildFolderTree(fileTree.value)
|
||||
})
|
||||
|
||||
|
||||
const currentFolderFiles = computed(() => {
|
||||
let currentFolder = fileTree.value[0] // 根目录
|
||||
|
||||
for (const pathItem of currentPath.value) {
|
||||
const found = currentFolder.children?.find(child => child.id === pathItem.id)
|
||||
if (found) {
|
||||
currentFolder = found
|
||||
}
|
||||
}
|
||||
|
||||
return currentFolder.children || []
|
||||
})
|
||||
|
||||
// 工具函数
|
||||
function buildFolderTree(nodes: FileNodeType[]): FileNodeType[] {
|
||||
return nodes.map(node => {
|
||||
if (node.type === NodeType.Folder) {
|
||||
const folderNode: FileNodeType = {
|
||||
...node,
|
||||
children: node.children ? buildFolderTree(node.children.filter(child => child.type === NodeType.Folder)) : []
|
||||
}
|
||||
return folderNode
|
||||
}
|
||||
return node
|
||||
}).filter(node => node.type === NodeType.Folder)
|
||||
}
|
||||
|
||||
function findNodeById(nodes: FileNodeType[], id: string): FileNodeType | null {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) {
|
||||
return node
|
||||
}
|
||||
if (node.children) {
|
||||
const found = findNodeById(node.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getFileIcon(type: NodeType): string {
|
||||
return FILE_ICONS[type] || '📄'
|
||||
}
|
||||
|
||||
function getFileTypeLabel(type: NodeType): string {
|
||||
const typeLabels: Record<NodeType, string> = {
|
||||
[NodeType.Text]: '文本',
|
||||
[NodeType.Folder]: '文件夹',
|
||||
[NodeType.Model]: '模型',
|
||||
[NodeType.Audio]: '音频',
|
||||
[NodeType.Video]: '视频',
|
||||
[NodeType.Material]: '材质',
|
||||
[NodeType.Texture]: '纹理',
|
||||
[NodeType.Image]: '图片',
|
||||
[NodeType.Script]: '脚本',
|
||||
[NodeType.Scene]: '场景'
|
||||
}
|
||||
return typeLabels[type] || '未知'
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
const handleFolderClick = (data: FileNodeType) => {
|
||||
selectedFolderId.value = data.id
|
||||
// 导航到选中的文件夹
|
||||
const pathToNode = getPathToNode(fileTree.value, data.id)
|
||||
currentPath.value = pathToNode.slice(1) // 移除根目录
|
||||
}
|
||||
|
||||
const handleFolderExpand = (data: FileNodeType) => {
|
||||
const targetNode = findNodeById(fileTree.value, data.id)
|
||||
if (targetNode) {
|
||||
targetNode.isExpanded = true
|
||||
}
|
||||
}
|
||||
|
||||
const handleFolderCollapse = (data: FileNodeType) => {
|
||||
const targetNode = findNodeById(fileTree.value, data.id)
|
||||
if (targetNode) {
|
||||
targetNode.isExpanded = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileClick = (node: FileNodeType) => {
|
||||
selectedFileId.value = node.id
|
||||
}
|
||||
|
||||
const handleFileRename = (node: FileNodeType, newName: string) => {
|
||||
const targetNode = findNodeById(fileTree.value, node.id)
|
||||
if (targetNode) {
|
||||
targetNode.name = newName
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileDoubleClick = (node: FileNodeType) => {
|
||||
if (node.type === NodeType.Folder) {
|
||||
// 进入文件夹
|
||||
currentPath.value.push(node)
|
||||
selectedFolderId.value = node.id
|
||||
selectedFileId.value = ''
|
||||
} else {
|
||||
// 打开文件
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽处理
|
||||
const handleDragStart = (node: FileNodeType, event: DragEvent) => {
|
||||
dragState.value.isDragging = true
|
||||
dragState.value.dragNode = node
|
||||
node.isDragging = true
|
||||
|
||||
if (event.dataTransfer) {
|
||||
cursorStore.setCursor('move')
|
||||
// event.dataTransfer.effectAllowed = 'move'
|
||||
// event.dataTransfer.setData('application/json', JSON.stringify(node))
|
||||
// event.dataTransfer.setData('text/plain', node.id)
|
||||
dragStore.setDragType(node.type)
|
||||
dragStore.setDragData(node)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragEnd = (node: FileNodeType, event: DragEvent) => {
|
||||
cursorStore.setCursor('default')
|
||||
dragState.value.isDragging = false
|
||||
dragState.value.dragNode = null
|
||||
dragState.value.dragOverNode = null
|
||||
dragState.value.dropPosition = null
|
||||
node.isDragging = false
|
||||
|
||||
}
|
||||
|
||||
const handleDragOver = (node: FileNodeType, event: DragEvent) => {
|
||||
if (dragState.value.dragNode && dragState.value.dragNode.id !== node.id) {
|
||||
event.preventDefault()
|
||||
dragState.value.dragOverNode = node
|
||||
dragState.value.dropPosition = 'inside'
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragLeave = (node: FileNodeType, event: DragEvent) => {
|
||||
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
|
||||
const x = event.clientX
|
||||
const y = event.clientY
|
||||
|
||||
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
|
||||
if (dragState.value.dragOverNode?.id === node.id) {
|
||||
dragState.value.dragOverNode = null
|
||||
dragState.value.dropPosition = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrop = (targetNode: FileNodeType, dragNode: FileNodeType, position: string, event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
|
||||
// 这里实现具体的拖拽逻辑
|
||||
}
|
||||
|
||||
// 导航功能
|
||||
const navigateToRoot = () => {
|
||||
currentPath.value = []
|
||||
selectedFolderId.value = fileTree.value[0].id
|
||||
selectedFileId.value = ''
|
||||
}
|
||||
|
||||
const navigateToFolder = (folder: FileNodeType, index: number) => {
|
||||
currentPath.value = currentPath.value.slice(0, index + 1)
|
||||
selectedFolderId.value = folder.id
|
||||
selectedFileId.value = ''
|
||||
}
|
||||
|
||||
function getPathToNode(nodes: FileNodeType[], targetId: string, currentPath: FileNodeType[] = []): FileNodeType[] {
|
||||
for (const node of nodes) {
|
||||
const newPath = [...currentPath, node]
|
||||
if (node.id === targetId) {
|
||||
return newPath
|
||||
}
|
||||
if (node.children) {
|
||||
const found = getPathToNode(node.children, targetId, newPath)
|
||||
if (found.length > 0) return found
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// 操作功能
|
||||
const toggleViewMode = () => {
|
||||
viewMode.value = viewMode.value === 'list' ? 'grid' : 'list'
|
||||
}
|
||||
|
||||
const createFolder = () => {
|
||||
const newFolder: FileNodeType = {
|
||||
id: generateId(),
|
||||
name: '新建文件夹',
|
||||
type: NodeType.Folder,
|
||||
path: '/新建文件夹',
|
||||
children: []
|
||||
}
|
||||
|
||||
// 添加到当前位置
|
||||
if (currentPath.value.length === 0) {
|
||||
fileTree.value[0].children = fileTree.value[0].children || []
|
||||
fileTree.value[0].children.push(newFolder)
|
||||
} else {
|
||||
const currentFolder = findNodeById(fileTree.value, currentPath.value[currentPath.value.length - 1].id)
|
||||
if (currentFolder) {
|
||||
currentFolder.children = currentFolder.children || []
|
||||
currentFolder.children.push(newFolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createFile = () => {
|
||||
const newFile: FileNodeType = {
|
||||
id: generateId(),
|
||||
name: '新建文件.txt',
|
||||
type: NodeType.Text,
|
||||
path: '/新建文件.txt',
|
||||
size: 0,
|
||||
lastModified: new Date()
|
||||
}
|
||||
|
||||
// 添加到当前文件夹
|
||||
if (currentPath.value.length === 0) {
|
||||
fileTree.value[0].children = fileTree.value[0].children || []
|
||||
fileTree.value[0].children.push(newFile)
|
||||
} else {
|
||||
const currentFolder = findNodeById(fileTree.value, currentPath.value[currentPath.value.length - 1].id)
|
||||
if (currentFolder) {
|
||||
currentFolder.children = currentFolder.children || []
|
||||
currentFolder.children.push(newFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uploadFile = () => {
|
||||
// 创建文件输入元素
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.multiple = true
|
||||
input.onchange = (e) => {
|
||||
const files = (e.target as HTMLInputElement).files
|
||||
if (files) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
const refreshTree = () => {
|
||||
}
|
||||
|
||||
// 拖拽调整面板大小
|
||||
let isResizing = false
|
||||
|
||||
const startResize = (event: MouseEvent) => {
|
||||
isResizing = true
|
||||
document.addEventListener('mousemove', handleResize)
|
||||
document.addEventListener('mouseup', stopResize)
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
const handleResize = (event: MouseEvent) => {
|
||||
if (!isResizing) return
|
||||
|
||||
const container = document.querySelector('.file-system') as HTMLElement
|
||||
if (!container) return
|
||||
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const newWidth = event.clientX - containerRect.left
|
||||
|
||||
if (newWidth >= minPanelWidth && newWidth <= maxPanelWidth) {
|
||||
leftPanelWidth.value = newWidth
|
||||
}
|
||||
}
|
||||
|
||||
const stopResize = () => {
|
||||
isResizing = false
|
||||
document.removeEventListener('mousemove', handleResize)
|
||||
document.removeEventListener('mouseup', stopResize)
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 初始化选中根目录
|
||||
selectedFolderId.value = fileTree.value[0].id
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', handleResize)
|
||||
document.removeEventListener('mouseup', stopResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-system {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background: #2d2d2d;
|
||||
color: #cccccc;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.file-tree-panel {
|
||||
background: #393939;
|
||||
border-right: 1px solid #4a4a4a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.tree-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
|
||||
.tree-node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tree-node-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tree-node-label {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.resize-handle {
|
||||
width: 4px;
|
||||
background: #4a4a4a;
|
||||
cursor: col-resize;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.resize-handle:hover {
|
||||
background: #409eff;
|
||||
}
|
||||
|
||||
.file-list-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #353535;
|
||||
border-bottom: 1px solid #4a4a4a;
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
cursor: pointer;
|
||||
color: #409eff;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.breadcrumb-item:hover {
|
||||
color: #66b1ff;
|
||||
}
|
||||
|
||||
/* 网格视图样式 */
|
||||
.file-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
|
||||
gap: 1px;
|
||||
padding: 8px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* 文件列表视图样式 */
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
padding: 8px 12px;
|
||||
background: #353535;
|
||||
border-bottom: 1px solid #4a4a4a;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.file-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
border-radius: 3px;
|
||||
margin: 1px 4px;
|
||||
}
|
||||
|
||||
.file-row:hover {
|
||||
background: #404040;
|
||||
}
|
||||
|
||||
.file-row.selected {
|
||||
background: #2d4a6b;
|
||||
}
|
||||
|
||||
.col-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.col-type {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.col-size {
|
||||
width: 80px;
|
||||
text-align: right;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.col-date {
|
||||
width: 120px;
|
||||
text-align: right;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.empty-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.empty-actions button {
|
||||
background: #409eff;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.empty-actions button:hover {
|
||||
background: #66b1ff;
|
||||
}
|
||||
|
||||
/* 文件类型特定样式 */
|
||||
.file-item.folder .file-item-name,
|
||||
.file-row.folder .file-name {
|
||||
color: #e6a23c;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-item.script .file-item-name,
|
||||
.file-row.script .file-name {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.file-item.model .file-item-name,
|
||||
.file-row.model .file-name {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.file-item.image .file-item-name,
|
||||
.file-item.texture .file-item-name,
|
||||
.file-row.image .file-name,
|
||||
.file-row.texture .file-name {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.file-item.material .file-item-name,
|
||||
.file-row.material .file-name {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.tree-content::-webkit-scrollbar,
|
||||
.list-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.tree-content::-webkit-scrollbar-track,
|
||||
.list-content::-webkit-scrollbar-track {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
.tree-content::-webkit-scrollbar-thumb,
|
||||
.list-content::-webkit-scrollbar-thumb {
|
||||
background: #4a4a4a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tree-content::-webkit-scrollbar-thumb:hover,
|
||||
.list-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #5a5a5a;
|
||||
}
|
||||
|
||||
/* Element Plus 树组件样式覆盖 */
|
||||
/* :deep(.el-tree) {
|
||||
background: transparent;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.tree-content :deep(.el-tree-node__content) {
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
margin: 2px 0;
|
||||
padding: 4px 8px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.tree-content :deep(.el-tree-node__content:hover) {
|
||||
background: #404040;
|
||||
}
|
||||
|
||||
.tree-content :deep(.el-tree-node.is-current > .el-tree-node__content) {
|
||||
background: #409eff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tree-content :deep(.el-tree-node__expand-icon) {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tree-content :deep(.el-tree-node__expand-icon.is-leaf) {
|
||||
color: transparent;
|
||||
}
|
||||
*/
|
||||
|
||||
.tree-node-label {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.el-tree {
|
||||
background: #1c00be00;
|
||||
}
|
||||
|
||||
|
||||
/* 设置节点的默认文字颜色 */
|
||||
::v-deep .el-tree-node__label {
|
||||
color: #ffffff;
|
||||
/* 自定义你想要的颜色 */
|
||||
}
|
||||
|
||||
/* 设置鼠标悬停时的背景颜色 */
|
||||
::v-deep .el-tree-node:hover>.el-tree-node__content {
|
||||
background-color: #f0f9eb00;
|
||||
/* 自定义悬停背景色 */
|
||||
}
|
||||
|
||||
/* 设置鼠标悬停时的文字颜色 */
|
||||
::v-deep .el-tree-node:hover .el-tree-node__label {
|
||||
color: #f8f8f8;
|
||||
/* 自定义悬停文字颜色 */
|
||||
}
|
||||
|
||||
::v-deep .el-tree-node__content {
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
/* 鼠标离开后恢复的背景色 */
|
||||
color: #333;
|
||||
/* 默认文字颜色 */
|
||||
}
|
||||
|
||||
::v-deep .el-tree-node.is-current>.el-tree-node__content {
|
||||
background-color: #ffffff28;
|
||||
}
|
||||
</style>
|
||||
428
src/components/BottomPanel/index.vue
Normal file
@ -0,0 +1,428 @@
|
||||
<template>
|
||||
<div class="bottom-panel" :style="{ height: layoutStore.bottomPanelHeight + 'px' }">
|
||||
<!-- 拖拽调整高度的手柄 -->
|
||||
<div
|
||||
class="resize-handle"
|
||||
@mousedown="startResize"
|
||||
@dblclick="resetHeight"
|
||||
>
|
||||
<div class="resize-indicator"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel-tabs">
|
||||
<div class="tab-list">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:class="['tab-button', { active: activeTab === tab.key }]"
|
||||
@click="setActiveTab(tab.key)"
|
||||
>
|
||||
<span class="tab-icon">{{ tab.icon }}</span>
|
||||
<span class="tab-label">{{ tab.label }}</span>
|
||||
<span v-if="tab.badge" class="tab-badge">{{ tab.badge }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-actions">
|
||||
<button @click="togglePanel" :title="layoutStore.bottomPanelCollapsed ? '展开面板' : '收起面板'">
|
||||
{{ layoutStore.bottomPanelCollapsed ? '⬆️' : '⬇️' }}
|
||||
</button>
|
||||
<button @click="closePanel" title="关闭面板">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="!layoutStore.bottomPanelCollapsed" class="panel-content">
|
||||
<!-- 文件系统 -->
|
||||
<div v-show="activeTab === 'files'" class="tab-content">
|
||||
<FileSystem ref="fileSystemRef" />
|
||||
</div>
|
||||
|
||||
<!-- 控制台 -->
|
||||
<div v-show="activeTab === 'console'" class="tab-content">
|
||||
<Console ref="consoleRef" />
|
||||
</div>
|
||||
|
||||
<!-- 终端 -->
|
||||
<div v-show="activeTab === 'terminal'" class="tab-content">
|
||||
<div class="terminal-placeholder">
|
||||
<div class="placeholder-content">
|
||||
<h3>终端</h3>
|
||||
<p>终端功能正在开发中...</p>
|
||||
<button @click="addTestTerminalOutput">添加测试输出</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输出 -->
|
||||
<div v-show="activeTab === 'output'" class="tab-content">
|
||||
<div class="output-placeholder">
|
||||
<div class="placeholder-content">
|
||||
<h3>输出</h3>
|
||||
<p>构建输出将显示在这里</p>
|
||||
<div class="output-example">
|
||||
<pre>{{ sampleOutput }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useLayoutStore } from '@/stores/Layout'
|
||||
import FileSystem from './FileSystem/index.vue'
|
||||
import Console from './Console/index.vue'
|
||||
|
||||
// 使用布局store
|
||||
const layoutStore = useLayoutStore()
|
||||
|
||||
// 标签页配置
|
||||
const tabs = [
|
||||
{ key: 'files', label: '文件', icon: '📁', badge: null },
|
||||
{ key: 'console', label: '控制台', icon: '🖥️', badge: null },
|
||||
{ key: 'terminal', label: '终端', icon: '⚡', badge: null },
|
||||
{ key: 'output', label: '输出', icon: '📄', badge: null }
|
||||
]
|
||||
|
||||
// 响应式数据
|
||||
const activeTab = ref('files')
|
||||
|
||||
// 组件引用
|
||||
const fileSystemRef = ref()
|
||||
const consoleRef = ref()
|
||||
|
||||
// 示例输出内容
|
||||
const sampleOutput = ref(`> 正在构建项目...
|
||||
✓ 编译 TypeScript 文件
|
||||
✓ 打包资源文件
|
||||
✓ 优化代码
|
||||
✓ 生成 source map
|
||||
✅ 构建完成!
|
||||
|
||||
输出目录: dist/
|
||||
构建时间: 2.3s
|
||||
文件大小: 1.2MB`)
|
||||
|
||||
// 计算属性
|
||||
const consoleBadge = computed(() => {
|
||||
if (consoleRef.value?.logs?.length > 0) {
|
||||
return consoleRef.value.logs.length.toString()
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
// 方法
|
||||
function setActiveTab(tabKey: string) {
|
||||
activeTab.value = tabKey
|
||||
}
|
||||
|
||||
function togglePanel() {
|
||||
layoutStore.toggleBottomPanelCollapse()
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
layoutStore.hideBottomPanel()
|
||||
}
|
||||
|
||||
function addTestTerminalOutput() {
|
||||
if (consoleRef.value) {
|
||||
consoleRef.value.info('终端命令执行: npm run build', { source: 'Terminal' })
|
||||
consoleRef.value.success('构建完成', { source: 'Terminal' })
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽调整高度功能
|
||||
function startResize(event: MouseEvent) {
|
||||
layoutStore.setResizing(true, 'bottom')
|
||||
const startY = event.clientY
|
||||
const startHeight = layoutStore.bottomPanelHeight
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!layoutStore.isResizing) return
|
||||
|
||||
const deltaY = startY - e.clientY // 向上拖拽为正值
|
||||
layoutStore.setBottomPanelHeight(startHeight + deltaY)
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
layoutStore.setResizing(false, null)
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
|
||||
// 保存布局配置
|
||||
layoutStore.saveLayoutConfig()
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
document.body.style.cursor = 'ns-resize'
|
||||
document.body.style.userSelect = 'none'
|
||||
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function resetHeight() {
|
||||
layoutStore.setBottomPanelHeight(300) // 重置到默认高度
|
||||
layoutStore.saveLayoutConfig()
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
showPanel: () => {
|
||||
layoutStore.showBottomPanel()
|
||||
},
|
||||
hidePanel: () => {
|
||||
layoutStore.hideBottomPanel()
|
||||
},
|
||||
togglePanel,
|
||||
setActiveTab,
|
||||
addLog: (type: string, message: string, options?: any) => {
|
||||
if (consoleRef.value) {
|
||||
consoleRef.value.addLog(type, message, options)
|
||||
// 自动切换到控制台标签
|
||||
if (type === 'error' || type === 'warn') {
|
||||
activeTab.value = 'console'
|
||||
}
|
||||
}
|
||||
},
|
||||
console: computed(() => consoleRef.value),
|
||||
fileSystem: computed(() => fileSystemRef.value)
|
||||
})
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 更新控制台徽章
|
||||
if (consoleRef.value) {
|
||||
const consoleTab = tabs.find(tab => tab.key === 'console')
|
||||
if (consoleTab) {
|
||||
consoleTab.badge = consoleBadge.value
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bottom-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #2d2d2d;
|
||||
color: #cccccc;
|
||||
border-top: 1px solid #3e3e3e;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 拖拽调整高度的手柄 */
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 6px;
|
||||
background: transparent;
|
||||
cursor: ns-resize;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.resize-handle:hover {
|
||||
background: rgba(0, 122, 204, 0.2);
|
||||
}
|
||||
|
||||
.resize-handle:active {
|
||||
background: rgba(0, 122, 204, 0.4);
|
||||
}
|
||||
|
||||
.resize-indicator {
|
||||
width: 40px;
|
||||
height: 3px;
|
||||
background: #4a4a4a;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.resize-handle:hover .resize-indicator {
|
||||
background: #007acc;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.resize-handle:active .resize-indicator {
|
||||
background: #005a9e;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.panel-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #3c3c3c;
|
||||
border-bottom: 1px solid #4a4a4a;
|
||||
|
||||
}
|
||||
|
||||
.tab-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #888;
|
||||
padding: 2px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
border-top: 2px solid transparent;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
background: #4a4a4a;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background: #2d2d2d;
|
||||
color: #ffffff;
|
||||
border-top-color: #007acc;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
.tab-badge {
|
||||
background: #f14c4c;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
min-width: 16px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tab-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.tab-actions button {
|
||||
background: transparent;
|
||||
border: 1px solid #4a4a4a;
|
||||
color: #888;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-actions button:hover {
|
||||
background: #4a4a4a;
|
||||
border-color: #007acc;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal-placeholder,
|
||||
.output-placeholder {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.placeholder-content h3 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #cccccc;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.placeholder-content p {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.placeholder-content button {
|
||||
background: #007acc;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.placeholder-content button:hover {
|
||||
background: #005a9e;
|
||||
}
|
||||
|
||||
.output-example {
|
||||
margin-top: 16px;
|
||||
text-align: left;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #3e3e3e;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.output-example pre {
|
||||
margin: 0;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: #d4d4d4;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.tab-button {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
95
src/components/HierarchyPanel/index.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="hierarchy-panel" @click="handlePanelClick">
|
||||
<el-tree ref="HierarchytreeRef" style="max-width: 600px" :data="HierarchytreeData" node-key="id"
|
||||
:props="defaultProps" @node-click="handleNodeClick">
|
||||
<template #default="{ node, data }">
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useHierarchyPanelStore } from 'stores/HierarchyPanel'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useInspectorPanelStore } from '@/stores/InspectorPanel'
|
||||
|
||||
const hierarchyPanelStore = useHierarchyPanelStore() // 获取HierarchyPanelStore 树结构pinia
|
||||
|
||||
const inspectorPanelStore = useInspectorPanelStore() // 获取InspectorPanelStore 面板pinia
|
||||
|
||||
const { selectedHierarchytreeData, HierarchytreeData } = storeToRefs(hierarchyPanelStore)
|
||||
|
||||
const HierarchytreeRef = ref()
|
||||
|
||||
|
||||
const defaultProps = ref({
|
||||
children: 'children',
|
||||
label: 'label'
|
||||
})
|
||||
|
||||
|
||||
watch(selectedHierarchytreeData, (node: any) => {
|
||||
// data.value = newVal
|
||||
if (node) {
|
||||
HierarchytreeRef.value.setCurrentKey(node.id)
|
||||
}
|
||||
else {
|
||||
HierarchytreeRef.value.setCurrentKey(null)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const handleNodeClick = (node: any) => {
|
||||
console.log(node);
|
||||
|
||||
hierarchyPanelStore.setSelectedData(node)
|
||||
inspectorPanelStore.setSelecteInspectorData(node)
|
||||
}
|
||||
|
||||
const handlePanelClick = () => {
|
||||
hierarchyPanelStore.setSelectedData(null)
|
||||
inspectorPanelStore.setSelecteInspectorData(null)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hierarchy-panel {
|
||||
height: 100%;
|
||||
border-right: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.el-tree {
|
||||
background: #1c00be00;
|
||||
}
|
||||
|
||||
/* 设置节点的默认文字颜色 */
|
||||
::v-deep .el-tree-node__label {
|
||||
color: #ffffff;
|
||||
/* 自定义你想要的颜色 */
|
||||
}
|
||||
|
||||
/* 设置鼠标悬停时的背景颜色 */
|
||||
::v-deep .el-tree-node:hover>.el-tree-node__content {
|
||||
background-color: #f0f9eb00;
|
||||
/* 自定义悬停背景色 */
|
||||
}
|
||||
|
||||
/* 设置鼠标悬停时的文字颜色 */
|
||||
::v-deep .el-tree-node:hover .el-tree-node__label {
|
||||
color: #f8f8f8;
|
||||
/* 自定义悬停文字颜色 */
|
||||
}
|
||||
|
||||
::v-deep .el-tree-node__content {
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
/* 鼠标离开后恢复的背景色 */
|
||||
color: #333;
|
||||
/* 默认文字颜色 */
|
||||
}
|
||||
|
||||
::v-deep .el-tree-node.is-current>.el-tree-node__content {
|
||||
background-color: #ffffff28;
|
||||
}
|
||||
</style>
|
||||
1
src/components/InspectorPanel/Camera/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export const contain=["Camera"]
|
||||
480
src/components/InspectorPanel/Camera/index.vue
Normal file
@ -0,0 +1,480 @@
|
||||
<template>
|
||||
<div class="camera-panel" v-show="visible">
|
||||
<!-- Camera 标题栏 -->
|
||||
<CollapsibleHeader title="相机" :expanded="isExpanded" :enabled="isEnabled" :checkable="false" @update:expanded="isExpanded = $event"
|
||||
@update:enabled="handleEnabledChange" @action-click="handleActionClick" />
|
||||
|
||||
<!-- Camera 内容 -->
|
||||
<div class="camera-content" v-show="isExpanded">
|
||||
<!-- 相机类型选择 -->
|
||||
<PropertyRow label="类型" tooltip="选择场景中使用的相机类型">
|
||||
<Select v-model="cameraData.type" :options="cameraTypeOptions" @change="onCameraTypeChange" />
|
||||
</PropertyRow>
|
||||
|
||||
<!-- ArcRotateCamera 属性 -->
|
||||
<template v-if="cameraData.type === 'ArcRotateCamera'">
|
||||
<!-- Target -->
|
||||
<PropertyRow label="目标位置" tooltip="相机观察的目标位置 (Vector3)">
|
||||
<div class="vector3-input">
|
||||
<div v-for="axis in vector3Axes" :key="axis.key" class="value-group">
|
||||
<span class="axis-label">{{ axis.label }}</span>
|
||||
<NumberInput :model-value="cameraData.arcRotate.target[axis.key]"
|
||||
@update:model-value="updateTargetAxis(axis.key, $event)" :step="0.1" />
|
||||
</div>
|
||||
</div>
|
||||
</PropertyRow>
|
||||
|
||||
<!-- 范围输入组件 -->
|
||||
<PropertyRow v-for="range in arcRotateRanges" :key="range.key" :label="range.label" :tooltip="range.tooltip">
|
||||
<div class="range-input">
|
||||
<div class="value-group">
|
||||
<span class="axis-label">最小</span>
|
||||
<NumberInput :model-value="cameraData.arcRotate[range.minKey]" @update:model-value="range.updateMin"
|
||||
:step="range.step" :min="range.min" />
|
||||
</div>
|
||||
<div class="value-group">
|
||||
<span class="axis-label">最大</span>
|
||||
<NumberInput :model-value="cameraData.arcRotate[range.maxKey]" @update:model-value="range.updateMax"
|
||||
:step="range.step" :min="range.min" />
|
||||
</div>
|
||||
</div>
|
||||
</PropertyRow>
|
||||
|
||||
<!-- 单值属性 -->
|
||||
<!-- Wheel Delta Percentage -->
|
||||
<PropertyRow label="滚轮灵敏度" tooltip="鼠标滚轮缩放灵敏度百分比">
|
||||
<NumberInput v-model="cameraData.arcRotate.wheelDeltaPercentage" @update:model-value="updateArcRotateCamera"
|
||||
:step="0.01" :min="0" :max="1" :full-width="true" />
|
||||
</PropertyRow>
|
||||
|
||||
<!-- Panning Sensibility -->
|
||||
<PropertyRow label="平移灵敏度" tooltip="平移灵敏度 - 数值越小越灵敏">
|
||||
<NumberInput v-model="cameraData.arcRotate.panningSensibility" @update:model-value="updateArcRotateCamera"
|
||||
:step="1" :min="1" :full-width="true" />
|
||||
</PropertyRow>
|
||||
|
||||
<!-- Inertia -->
|
||||
<PropertyRow label="惯性" tooltip="移动惯性 (0-1),默认 0.9">
|
||||
<Slider v-model="cameraData.arcRotate.inertia" @change="updateArcRotateCamera" :min="0" :max="1"
|
||||
:step="0.01" />
|
||||
</PropertyRow>
|
||||
|
||||
<!-- Auto Rotation -->
|
||||
<PropertyRow label="自动旋转" tooltip="启用相机自动旋转">
|
||||
<Switch v-model="cameraData.arcRotate.autoRotation" @change="updateArcRotateCamera" size="small" />
|
||||
</PropertyRow>
|
||||
</template>
|
||||
|
||||
<!-- UniversalCamera 属性 -->
|
||||
<template v-if="cameraData.type === 'UniversalCamera'">
|
||||
<!-- 数值属性 -->
|
||||
<PropertyRow v-for="prop in universalNumericProps" :key="prop.key" :label="prop.label" :tooltip="prop.tooltip">
|
||||
<NumberInput v-model="cameraData.universal[prop.key]" @update:model-value="updateUniversalCamera"
|
||||
v-bind="prop.props" />
|
||||
</PropertyRow>
|
||||
|
||||
<!-- 键位绑定 -->
|
||||
<PropertyRow v-for="keyProp in universalKeyProps" :key="keyProp.key" :label="keyProp.label"
|
||||
:tooltip="keyProp.tooltip">
|
||||
<TextInput v-model="cameraData.universal[keyProp.key]" @update:model-value="updateUniversalCamera"
|
||||
:placeholder="keyProp.placeholder" text-align="center" :uppercase="true" />
|
||||
</PropertyRow>
|
||||
</template>
|
||||
|
||||
<!-- VR Camera 属性 -->
|
||||
<template v-if="cameraData.type === 'vrCamera'">
|
||||
<!-- 数值属性 -->
|
||||
<PropertyRow v-for="prop in vrNumericProps" :key="prop.key" :label="prop.label" :tooltip="prop.tooltip">
|
||||
<NumberInput v-model="cameraData.vr[prop.key]" @update:model-value="updateVrCamera" v-bind="prop.props" />
|
||||
</PropertyRow>
|
||||
|
||||
<!-- VR 范围输入 -->
|
||||
<PropertyRow v-for="range in vrRanges" :key="range.key" :label="range.label" :tooltip="range.tooltip">
|
||||
<div class="range-input">
|
||||
<div class="value-group">
|
||||
<span class="axis-label">最小</span>
|
||||
<NumberInput :model-value="cameraData.vr[range.minKey]" @update:model-value="range.updateMin"
|
||||
:step="range.step" :min="range.min" />
|
||||
</div>
|
||||
<div class="value-group">
|
||||
<span class="axis-label">最大</span>
|
||||
<NumberInput :model-value="cameraData.vr[range.maxKey]" @update:model-value="range.updateMax"
|
||||
:step="range.step" :min="range.min" />
|
||||
</div>
|
||||
</div>
|
||||
</PropertyRow>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, reactive, computed } from 'vue'
|
||||
import { useInspectorPanelStore } from 'stores/InspectorPanel'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import CollapsibleHeader from 'components/public/CollapsibleHeader.vue'
|
||||
import PropertyRow from 'components/BasicControls/PropertyRow/index.vue'
|
||||
import Select from 'components/BasicControls/Select/index.vue'
|
||||
import NumberInput from 'components/BasicControls/Input/index.vue'
|
||||
import Slider from 'components/BasicControls/Slider/index.vue'
|
||||
import Switch from 'components/BasicControls/Switch/index.vue'
|
||||
import TextInput from 'components/BasicControls/Input/TextInput.vue'
|
||||
import { useInspectorPanel, DEFAULT_COMPONENT_CONFIGS } from '../../../composables/useInspectorPanel'
|
||||
import type { SelectOption } from 'components/BasicControls/Select/types'
|
||||
import type {
|
||||
Vector3,
|
||||
CameraData,
|
||||
} from './types'
|
||||
|
||||
import { contain } from "./index"
|
||||
|
||||
const inspectorPanelStore = useInspectorPanelStore()
|
||||
const { selecteInspectorData } = storeToRefs(inspectorPanelStore)
|
||||
|
||||
// 使用通用的 Inspector Panel 逻辑
|
||||
const {
|
||||
isExpanded,
|
||||
isEnabled,
|
||||
createEnabledChangeHandler,
|
||||
createActionClickHandler,
|
||||
createShowMoreHandler
|
||||
} = useInspectorPanel()
|
||||
|
||||
// 相机类型选项
|
||||
const cameraTypeOptions: SelectOption[] = [
|
||||
{ value: 'ArcRotateCamera', label: '弧形旋转相机' },
|
||||
{ value: 'UniversalCamera', label: '通用相机' },
|
||||
{ value: 'vrCamera', label: 'VR相机' }
|
||||
]
|
||||
|
||||
// 相机数据
|
||||
const cameraData = reactive<CameraData>({
|
||||
type: 'ArcRotateCamera',
|
||||
arcRotate: { ...DEFAULT_COMPONENT_CONFIGS.camera.arcRotate },
|
||||
universal: { ...DEFAULT_COMPONENT_CONFIGS.camera.universal },
|
||||
vr: { ...DEFAULT_COMPONENT_CONFIGS.camera.vr }
|
||||
})
|
||||
|
||||
// Vector3 轴配置
|
||||
const vector3Axes = [
|
||||
{ key: 'x' as keyof Vector3, label: 'X' },
|
||||
{ key: 'y' as keyof Vector3, label: 'Y' },
|
||||
{ key: 'z' as keyof Vector3, label: 'Z' }
|
||||
]
|
||||
const visible = ref<any>({})
|
||||
const inspectorPanelData = ref<any>(
|
||||
{
|
||||
id: "",
|
||||
label: '',
|
||||
type: "",
|
||||
}
|
||||
)
|
||||
watch(selecteInspectorData, (node: any) => {
|
||||
if (!node) {
|
||||
visible.value = false
|
||||
return;
|
||||
}
|
||||
if (contain.includes(node.type)) {
|
||||
visible.value = true
|
||||
} else {
|
||||
visible.value = false
|
||||
}
|
||||
inspectorPanelData.value = node
|
||||
})
|
||||
|
||||
|
||||
// 更新函数
|
||||
const updateArcRotateCamera = () => {
|
||||
// console.log('弧形旋转相机已更新:', cameraData.arcRotate)
|
||||
// 这里可以添加实际的更新逻辑
|
||||
}
|
||||
|
||||
const updateUniversalCamera = () => {
|
||||
//console.log('通用相机已更新:', cameraData.universal)
|
||||
// 这里可以添加实际的更新逻辑
|
||||
}
|
||||
|
||||
const updateVrCamera = () => {
|
||||
//console.log('VR相机已更新:', cameraData.vr)
|
||||
// 这里可以添加实际的更新逻辑
|
||||
}
|
||||
|
||||
// 范围更新函数
|
||||
const updateBetaMin = (value: number) => {
|
||||
cameraData.arcRotate.lowerBetaLimit = value
|
||||
updateArcRotateCamera()
|
||||
}
|
||||
|
||||
const updateBetaMax = (value: number) => {
|
||||
cameraData.arcRotate.upperBetaLimit = value
|
||||
updateArcRotateCamera()
|
||||
}
|
||||
|
||||
const updateRadiusMin = (value: number) => {
|
||||
cameraData.arcRotate.lowerRadiusLimit = value
|
||||
updateArcRotateCamera()
|
||||
}
|
||||
|
||||
const updateRadiusMax = (value: number) => {
|
||||
cameraData.arcRotate.upperRadiusLimit = value
|
||||
updateArcRotateCamera()
|
||||
}
|
||||
|
||||
const updateVrBetaMin = (value: number) => {
|
||||
cameraData.vr.lowerBetaLimit = value
|
||||
updateVrCamera()
|
||||
}
|
||||
|
||||
const updateVrBetaMax = (value: number) => {
|
||||
cameraData.vr.upperBetaLimit = value
|
||||
updateVrCamera()
|
||||
}
|
||||
|
||||
const updateVrRadiusMin = (value: number) => {
|
||||
cameraData.vr.lowerRadiusLimit = value
|
||||
updateVrCamera()
|
||||
}
|
||||
|
||||
const updateVrRadiusMax = (value: number) => {
|
||||
cameraData.vr.upperRadiusLimit = value
|
||||
updateVrCamera()
|
||||
}
|
||||
|
||||
const updateTargetAxis = (axis: keyof Vector3, value: number) => {
|
||||
cameraData.arcRotate.target[axis] = value
|
||||
updateArcRotateCamera()
|
||||
}
|
||||
|
||||
// 弧形旋转相机范围配置
|
||||
const arcRotateRanges = [
|
||||
{
|
||||
key: 'beta',
|
||||
label: 'Beta角度限制',
|
||||
tooltip: '垂直角度限制 (弧度)',
|
||||
minKey: 'lowerBetaLimit' as const,
|
||||
maxKey: 'upperBetaLimit' as const,
|
||||
step: 0.1,
|
||||
updateMin: updateBetaMin,
|
||||
updateMax: updateBetaMax
|
||||
},
|
||||
{
|
||||
key: 'radius',
|
||||
label: '半径限制',
|
||||
tooltip: '距离目标的缩放距离限制',
|
||||
minKey: 'lowerRadiusLimit' as const,
|
||||
maxKey: 'upperRadiusLimit' as const,
|
||||
step: 0.1,
|
||||
min: 0,
|
||||
updateMin: updateRadiusMin,
|
||||
updateMax: updateRadiusMax
|
||||
}
|
||||
]
|
||||
|
||||
// 通用相机数值属性配置
|
||||
const universalNumericProps = [
|
||||
{
|
||||
key: 'speed' as const,
|
||||
label: '移动速度',
|
||||
tooltip: '相机的移动速度',
|
||||
props: { step: 0.1, min: 0, 'full-width': true }
|
||||
},
|
||||
{
|
||||
key: 'angularSensibility' as const,
|
||||
label: '旋转灵敏度',
|
||||
tooltip: '鼠标旋转灵敏度',
|
||||
props: { step: 1, min: 1, 'full-width': true }
|
||||
}
|
||||
]
|
||||
|
||||
// 通用相机键位属性配置
|
||||
const universalKeyProps = [
|
||||
{
|
||||
key: 'keysUp' as const,
|
||||
label: '前进键',
|
||||
tooltip: '相机移动的键位绑定 (默认WASD)',
|
||||
placeholder: 'W'
|
||||
},
|
||||
{
|
||||
key: 'keysDown' as const,
|
||||
label: '后退键',
|
||||
tooltip: '相机移动的键位绑定 (默认WASD)',
|
||||
placeholder: 'S'
|
||||
},
|
||||
{
|
||||
key: 'keysLeft' as const,
|
||||
label: '左移键',
|
||||
tooltip: '相机移动的键位绑定 (默认WASD)',
|
||||
placeholder: 'A'
|
||||
},
|
||||
{
|
||||
key: 'keysRight' as const,
|
||||
label: '右移键',
|
||||
tooltip: '相机移动的键位绑定 (默认WASD)',
|
||||
placeholder: 'D'
|
||||
}
|
||||
]
|
||||
|
||||
// VR相机数值属性配置
|
||||
const vrNumericProps = [
|
||||
{
|
||||
key: 'angularSensibility' as const,
|
||||
label: '旋转灵敏度',
|
||||
tooltip: 'VR相机的鼠标旋转灵敏度',
|
||||
props: { step: 1, min: 1, 'full-width': true }
|
||||
},
|
||||
{
|
||||
key: 'wheelDeltaPercentage' as const,
|
||||
label: '滚轮灵敏度',
|
||||
tooltip: 'VR相机的鼠标滚轮缩放灵敏度',
|
||||
props: { step: 0.01, min: 0, max: 1, 'full-width': true }
|
||||
}
|
||||
]
|
||||
|
||||
// VR相机范围配置
|
||||
const vrRanges = [
|
||||
{
|
||||
key: 'beta',
|
||||
label: 'Beta角度限制',
|
||||
tooltip: 'VR相机的垂直角度限制 (弧度)',
|
||||
minKey: 'lowerBetaLimit' as const,
|
||||
maxKey: 'upperBetaLimit' as const,
|
||||
step: 0.1,
|
||||
updateMin: updateVrBetaMin,
|
||||
updateMax: updateVrBetaMax
|
||||
},
|
||||
{
|
||||
key: 'radius',
|
||||
label: '半径限制',
|
||||
tooltip: 'VR相机的缩放距离限制',
|
||||
minKey: 'lowerRadiusLimit' as const,
|
||||
maxKey: 'upperRadiusLimit' as const,
|
||||
step: 0.1,
|
||||
min: 0,
|
||||
updateMin: updateVrRadiusMin,
|
||||
updateMax: updateVrRadiusMax
|
||||
}
|
||||
]
|
||||
|
||||
// 相机类型变化处理
|
||||
const onCameraTypeChange = () => {
|
||||
console.log('相机类型已更改为:', cameraData.type)
|
||||
// 这里可以添加相机类型切换的逻辑
|
||||
}
|
||||
|
||||
// 重置相机
|
||||
const resetCamera = () => {
|
||||
switch (cameraData.type) {
|
||||
case 'ArcRotateCamera':
|
||||
Object.assign(cameraData.arcRotate, DEFAULT_COMPONENT_CONFIGS.camera.arcRotate)
|
||||
updateArcRotateCamera()
|
||||
break
|
||||
case 'UniversalCamera':
|
||||
Object.assign(cameraData.universal, DEFAULT_COMPONENT_CONFIGS.camera.universal)
|
||||
updateUniversalCamera()
|
||||
break
|
||||
case 'vrCamera':
|
||||
Object.assign(cameraData.vr, DEFAULT_COMPONENT_CONFIGS.camera.vr)
|
||||
updateVrCamera()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 处理启用状态变化
|
||||
const handleEnabledChange = createEnabledChangeHandler('Camera')
|
||||
|
||||
// 处理操作按钮点击
|
||||
const handleActionClick = createActionClickHandler(
|
||||
resetCamera,
|
||||
createShowMoreHandler('Camera')
|
||||
)
|
||||
|
||||
// 监听选中对象变化
|
||||
watch(selecteInspectorData, (node: any) => {
|
||||
if (node && node.camera) {
|
||||
// 更新相机数据
|
||||
const camera = node.camera
|
||||
if (camera.type) {
|
||||
cameraData.type = camera.type
|
||||
}
|
||||
|
||||
// 根据相机类型更新对应数据
|
||||
if (camera.type === 'ArcRotateCamera' && camera.arcRotate) {
|
||||
Object.assign(cameraData.arcRotate, camera.arcRotate)
|
||||
} else if (camera.type === 'UniversalCamera' && camera.universal) {
|
||||
Object.assign(cameraData.universal, camera.universal)
|
||||
} else if (camera.type === 'vrCamera' && camera.vr) {
|
||||
Object.assign(cameraData.vr, camera.vr)
|
||||
}
|
||||
|
||||
isEnabled.value = camera.isEnabled !== false
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.camera-panel {
|
||||
background: #393939;
|
||||
border-bottom: 1px solid #2d2d2d;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.camera-content {
|
||||
padding: 4px 8px 8px 8px;
|
||||
}
|
||||
|
||||
.vector3-input {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vector3-input .value-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.vector3-input .axis-label {
|
||||
width: 20px;
|
||||
color: #888888;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.range-input {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.range-input .value-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.range-input .axis-label {
|
||||
width: 20px;
|
||||
color: #888888;
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 容器自适应 */
|
||||
@container (max-width: 200px) {
|
||||
|
||||
.vector3-input .axis-label,
|
||||
.range-input .axis-label {
|
||||
width: 15px;
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
57
src/components/InspectorPanel/Camera/types.ts
Normal file
@ -0,0 +1,57 @@
|
||||
// Camera 组件类型定义
|
||||
|
||||
export interface Vector3 {
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
}
|
||||
|
||||
export interface ArcRotateCameraData {
|
||||
target: Vector3
|
||||
lowerBetaLimit: number
|
||||
upperBetaLimit: number
|
||||
lowerRadiusLimit: number
|
||||
upperRadiusLimit: number
|
||||
wheelDeltaPercentage: number
|
||||
panningSensibility: number
|
||||
inertia: number
|
||||
autoRotation: boolean
|
||||
}
|
||||
|
||||
export interface UniversalCameraData {
|
||||
speed: number
|
||||
angularSensibility: number
|
||||
keysUp: string
|
||||
keysDown: string
|
||||
keysLeft: string
|
||||
keysRight: string
|
||||
}
|
||||
|
||||
export interface VrCameraData {
|
||||
angularSensibility: number
|
||||
lowerBetaLimit: number
|
||||
upperBetaLimit: number
|
||||
lowerRadiusLimit: number
|
||||
upperRadiusLimit: number
|
||||
wheelDeltaPercentage: number
|
||||
}
|
||||
|
||||
export interface CameraData {
|
||||
type: 'ArcRotateCamera' | 'UniversalCamera' | 'vrCamera'
|
||||
arcRotate: ArcRotateCameraData
|
||||
universal: UniversalCameraData
|
||||
vr: VrCameraData
|
||||
}
|
||||
|
||||
export interface CameraTypeOption {
|
||||
label: string
|
||||
value: 'ArcRotateCamera' | 'UniversalCamera' | 'vrCamera'
|
||||
}
|
||||
|
||||
export interface CameraProps {
|
||||
// 如果有 props 的话可以在这里定义
|
||||
}
|
||||
|
||||
export interface CameraEmits {
|
||||
// 如果有 emits 的话可以在这里定义
|
||||
}
|
||||
255
src/components/InspectorPanel/Header/index.vue
Normal file
@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div class="inspector-header-panel">
|
||||
<div class="header-content">
|
||||
<!-- 左侧图标和激活状态 -->
|
||||
<div class="header-left">
|
||||
<Switch v-model="isActive" @change="toggleActive" size="small" />
|
||||
<!-- 对象名称输入框 -->
|
||||
<div class="object-name">
|
||||
<TextInput v-model="objectName" placeholder="GameObject" @update:modelValue="updateObjectName"
|
||||
class="name-input-wrapper" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<div class="header-right">
|
||||
<!-- 透明模式 -->
|
||||
<PropertyRow label="图层" tooltip="图层">
|
||||
<Select v-model="selectedLayer" :options="layerOptions" @change="updateLayer" />
|
||||
</PropertyRow>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { useInspectorPanelStore } from 'stores/InspectorPanel'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Switch from 'components/BasicControls/Switch/index.vue'
|
||||
import TextInput from 'components/BasicControls/Input/TextInput.vue'
|
||||
import Select from 'components/BasicControls/Select/index.vue'
|
||||
import PropertyRow from 'components/BasicControls/PropertyRow/index.vue'
|
||||
import type { SelectOption } from 'components/BasicControls/Select/types'
|
||||
|
||||
const inspectorPanelStore = useInspectorPanelStore()
|
||||
const { selecteInspectorData } = storeToRefs(inspectorPanelStore)
|
||||
|
||||
// 对象属性
|
||||
const isActive = ref(true)
|
||||
const objectName = ref('Directional Light')
|
||||
const isStatic = ref(false)
|
||||
const selectedTag = ref('Untagged')
|
||||
const selectedLayer = ref('Default')
|
||||
|
||||
// 图层选项
|
||||
const layerOptions: SelectOption[] = [
|
||||
{ value: 'Default', label: '默认' },
|
||||
{ value: 'TransparentFX', label: '透明特效' },
|
||||
{ value: 'Ignore Raycast', label: '忽略射线' },
|
||||
{ value: 'Water', label: '水面' },
|
||||
{ value: 'UI', label: '用户界面' },
|
||||
{ value: 'PostProcessing', label: '后期处理' }
|
||||
]
|
||||
|
||||
// 切换激活状态
|
||||
const toggleActive = () => {
|
||||
if (selecteInspectorData.value) {
|
||||
(selecteInspectorData.value as any).setEnabled(isActive.value)
|
||||
console.log(`对象${isActive.value ? '已激活' : '已停用'}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新对象名称
|
||||
const updateObjectName = () => {
|
||||
if (selecteInspectorData.value && objectName.value.trim()) {
|
||||
(selecteInspectorData.value as any).name = objectName.value.trim()
|
||||
console.log('对象名称已更新:', objectName.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新Static状态
|
||||
const updateStatic = () => {
|
||||
if (selecteInspectorData.value) {
|
||||
console.log(`静态${isStatic.value ? '已启用' : '已禁用'}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新Tag
|
||||
const updateTag = () => {
|
||||
if (selecteInspectorData.value) {
|
||||
console.log(`标签已更改为 ${selectedTag.value}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新Layer
|
||||
const updateLayer = () => {
|
||||
if (selecteInspectorData.value) {
|
||||
console.log(`图层已更改为 ${selectedLayer.value}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听选中对象变化
|
||||
watch(selecteInspectorData, (node: any) => {
|
||||
if (node) {
|
||||
// 更新界面数据
|
||||
objectName.value = node.label || 'GameObject'
|
||||
isActive.value = node.isEnabled !== false
|
||||
console.log(node);
|
||||
|
||||
// 根据对象类型设置不同的默认值
|
||||
if (node.label?.includes('Light')) {
|
||||
selectedTag.value = 'Untagged'
|
||||
selectedLayer.value = 'Default'
|
||||
} else if (node.label?.includes('Camera')) {
|
||||
selectedTag.value = 'MainCamera'
|
||||
selectedLayer.value = 'Default'
|
||||
} else {
|
||||
selectedTag.value = 'Untagged'
|
||||
selectedLayer.value = 'Default'
|
||||
}
|
||||
} else {
|
||||
// 重置为默认值
|
||||
objectName.value = 'GameObject'
|
||||
isActive.value = true
|
||||
isStatic.value = false
|
||||
selectedTag.value = 'Untagged'
|
||||
selectedLayer.value = 'Default'
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.inspector-header-panel {
|
||||
background: #393939;
|
||||
border-bottom: 1px solid #2d2d2d;
|
||||
padding: 8px 12px;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.object-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: #4a4a4a;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #5a5a5a;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.object-name {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.name-input-wrapper {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.name-input-wrapper :deep(.text-input) {
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.static-item {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.property-label {
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 自定义复选框 */
|
||||
.checkbox-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.custom-checkbox {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.checkbox-label:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.custom-checkbox:checked+.checkbox-label {
|
||||
background: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.custom-checkbox:checked+.checkbox-label::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 1px;
|
||||
width: 6px;
|
||||
height: 9px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.object-properties {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.property-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
31
src/components/InspectorPanel/Header/types.ts
Normal file
@ -0,0 +1,31 @@
|
||||
// Header 组件类型定义
|
||||
|
||||
export interface HeaderData {
|
||||
isActive: boolean
|
||||
objectName: string
|
||||
isStatic: boolean
|
||||
selectedTag: string
|
||||
selectedLayer: string
|
||||
}
|
||||
|
||||
export interface HeaderProps {
|
||||
// 如果有 props 的话可以在这里定义
|
||||
}
|
||||
|
||||
export interface HeaderEmits {
|
||||
// 如果有 emits 的话可以在这里定义
|
||||
}
|
||||
|
||||
export type TagType = 'Untagged' | 'Respawn' | 'Finish' | 'EditorOnly' | 'MainCamera' | 'Player' | 'GameController'
|
||||
|
||||
export type LayerType = 'Default' | 'TransparentFX' | 'Ignore Raycast' | 'Water' | 'UI' | 'PostProcessing'
|
||||
|
||||
export interface TagOption {
|
||||
label: string
|
||||
value: TagType
|
||||
}
|
||||
|
||||
export interface LayerOption {
|
||||
label: string
|
||||
value: LayerType
|
||||
}
|
||||
1
src/components/InspectorPanel/Light/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export const contain=["Light"]
|
||||
181
src/components/InspectorPanel/Light/index.vue
Normal file
@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="light-panel" v-show="visible">
|
||||
<CollapsibleHeader
|
||||
title="灯光"
|
||||
:expanded="isExpanded"
|
||||
:enabled="isEnabled"
|
||||
:checkable="false"
|
||||
@update:expanded="isExpanded = $event"
|
||||
@update:enabled="handleEnabledChange"
|
||||
@action-click="handleActionClick"
|
||||
>
|
||||
<div class="light-content">
|
||||
<!-- Type 属性 -->
|
||||
<PropertyRow
|
||||
label="类型"
|
||||
tooltip="指定当前光源的类型。可能的类型有方向光、聚光灯、点光源和区域光。"
|
||||
>
|
||||
<Select
|
||||
v-model="lightData.type"
|
||||
:options="lightTypeOptions"
|
||||
@change="updateLightType"
|
||||
/>
|
||||
</PropertyRow>
|
||||
|
||||
<!-- Color 属性 -->
|
||||
<PropertyRow
|
||||
label="颜色"
|
||||
tooltip="此光源发出的光的颜色。"
|
||||
>
|
||||
<ColorInput
|
||||
:model-value="lightData.color"
|
||||
@update:model-value="updateLightColor"
|
||||
/>
|
||||
</PropertyRow>
|
||||
|
||||
<!-- Intensity 属性 -->
|
||||
<PropertyRow
|
||||
label="强度"
|
||||
tooltip="光的亮度。数值越高产生的光越亮"
|
||||
>
|
||||
<Slider
|
||||
v-model="lightData.intensity"
|
||||
:min="0"
|
||||
:max="10"
|
||||
:step="0.1"
|
||||
:show-value="true"
|
||||
@change="updateLightIntensity"
|
||||
/>
|
||||
</PropertyRow>
|
||||
|
||||
</div>
|
||||
</CollapsibleHeader>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { useInspectorPanelStore } from 'stores/InspectorPanel'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import CollapsibleHeader from 'components/public/CollapsibleHeader.vue'
|
||||
import PropertyRow from 'components/BasicControls/PropertyRow/index.vue'
|
||||
import Select from 'components/BasicControls/Select/index.vue'
|
||||
import ColorInput from 'components/BasicControls/Input/ColorInput.vue'
|
||||
import Slider from 'components/BasicControls/Slider/index.vue'
|
||||
import Switch from 'components/BasicControls/Switch/index.vue'
|
||||
import { useInspectorPanel, DEFAULT_COMPONENT_CONFIGS, colorUtils } from '../../../composables/useInspectorPanel'
|
||||
import {contain} from "./index"
|
||||
const inspectorPanelStore = useInspectorPanelStore()
|
||||
const { selecteInspectorData } = storeToRefs(inspectorPanelStore)
|
||||
|
||||
// 使用通用的 Inspector Panel 逻辑
|
||||
const {
|
||||
isExpanded,
|
||||
isEnabled,
|
||||
createEnabledChangeHandler,
|
||||
createActionClickHandler,
|
||||
createShowMoreHandler
|
||||
} = useInspectorPanel()
|
||||
|
||||
// Light数据
|
||||
const lightData = reactive({ ...DEFAULT_COMPONENT_CONFIGS.light })
|
||||
|
||||
// 光源类型选项
|
||||
const lightTypeOptions = [
|
||||
{ label: '方向光', value: 'directional' },
|
||||
{ label: '点光源', value: 'point' },
|
||||
{ label: '聚光灯', value: 'spot' },
|
||||
{ label: '区域光', value: 'area' }
|
||||
]
|
||||
|
||||
const visible = ref<any>({})
|
||||
const inspectorPanelData = ref<any>({})
|
||||
watch(selecteInspectorData, (node: any) => {
|
||||
if (!node) {
|
||||
visible.value = false
|
||||
return;
|
||||
}
|
||||
|
||||
if(contain.includes(node.type)){
|
||||
visible.value = true
|
||||
}else{
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
inspectorPanelData.value = node
|
||||
})
|
||||
|
||||
|
||||
|
||||
// 更新函数
|
||||
const updateLightType = () => {
|
||||
if (selecteInspectorData.value && (selecteInspectorData.value as any)?.lightType) {
|
||||
(selecteInspectorData.value as any).lightType = lightData.type
|
||||
}
|
||||
}
|
||||
|
||||
const updateLightColor = (color: string) => {
|
||||
lightData.color = color
|
||||
if (selecteInspectorData.value && (selecteInspectorData.value as any)?.lightColor) {
|
||||
const rgb = colorUtils.hexStringToRgb(color)
|
||||
const lightColorObj = (selecteInspectorData.value as any).lightColor
|
||||
lightColorObj.r = rgb.r
|
||||
lightColorObj.g = rgb.g
|
||||
lightColorObj.b = rgb.b
|
||||
}
|
||||
}
|
||||
|
||||
const updateLightIntensity = () => {
|
||||
if (selecteInspectorData.value && (selecteInspectorData.value as any)?.lightIntensity !== undefined) {
|
||||
(selecteInspectorData.value as any).lightIntensity = lightData.intensity
|
||||
}
|
||||
}
|
||||
|
||||
// 重置Light
|
||||
const resetLight = () => {
|
||||
Object.assign(lightData, DEFAULT_COMPONENT_CONFIGS.light)
|
||||
updateLightType()
|
||||
updateLightColor(lightData.color)
|
||||
updateLightIntensity()
|
||||
}
|
||||
|
||||
// 处理启用状态变化
|
||||
const handleEnabledChange = createEnabledChangeHandler('光源', (enabled: boolean) => {
|
||||
if (selecteInspectorData.value && (selecteInspectorData.value as any)?.setEnabled) {
|
||||
(selecteInspectorData.value as any).setEnabled(enabled)
|
||||
}
|
||||
})
|
||||
|
||||
// 处理操作按钮点击
|
||||
const handleActionClick = createActionClickHandler(
|
||||
resetLight,
|
||||
createShowMoreHandler('光源')
|
||||
)
|
||||
|
||||
// 监听选中对象变化
|
||||
watch(selecteInspectorData, (node: any) => {
|
||||
if (node) {
|
||||
// 更新Light数据
|
||||
if (node.lightType) {
|
||||
lightData.type = node.lightType
|
||||
}
|
||||
if (node.lightColor) {
|
||||
lightData.color = colorUtils.rgbToHexString(node.lightColor)
|
||||
}
|
||||
if (node.lightIntensity !== undefined) {
|
||||
lightData.intensity = node.lightIntensity
|
||||
}
|
||||
isEnabled.value = node.isEnabled !== false
|
||||
} else {
|
||||
// 重置为默认值
|
||||
Object.assign(lightData, DEFAULT_COMPONENT_CONFIGS.light)
|
||||
isEnabled.value = true
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.light-content {
|
||||
/* Remove redundant styling since CollapsibleHeader handles container */
|
||||
}
|
||||
</style>
|
||||
28
src/components/InspectorPanel/Light/types.ts
Normal file
@ -0,0 +1,28 @@
|
||||
// Light 组件类型定义
|
||||
|
||||
export interface RGB {
|
||||
r: number
|
||||
g: number
|
||||
b: number
|
||||
}
|
||||
|
||||
export interface LightData {
|
||||
type: 'directional' | 'point' | 'spot' | 'area'
|
||||
color: string
|
||||
intensity: number
|
||||
}
|
||||
|
||||
export interface LightProps {
|
||||
// 如果有 props 的话可以在这里定义
|
||||
}
|
||||
|
||||
export interface LightEmits {
|
||||
// 如果有 emits 的话可以在这里定义
|
||||
}
|
||||
|
||||
export type LightType = 'directional' | 'point' | 'spot' | 'area'
|
||||
|
||||
export interface LightTypeOption {
|
||||
label: string
|
||||
value: LightType
|
||||
}
|
||||
1
src/components/InspectorPanel/Material/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export const contain=["Material"]
|
||||
197
src/components/InspectorPanel/Material/index.vue
Normal file
@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<div class="material-panel" v-show="visible">
|
||||
<CollapsibleHeader title="材质"
|
||||
:expanded="isExpanded"
|
||||
:enabled="isEnabled"
|
||||
:checkable="false"
|
||||
@update:expanded="isExpanded = $event"
|
||||
@update:enabled="handleEnabledChange"
|
||||
@action-click="handleActionClick">
|
||||
<div class="material-content">
|
||||
<!-- 材质类型 -->
|
||||
<PropertyRow
|
||||
label="材质类型"
|
||||
tooltip="材质类型"
|
||||
>
|
||||
<Select
|
||||
v-model="materialData.type"
|
||||
:options="materialTypeOptions"
|
||||
@change="updateMaterialProperty('type', $event)"
|
||||
/>
|
||||
</PropertyRow>
|
||||
|
||||
<!-- 基础颜色 -->
|
||||
<PropertyRow
|
||||
label="基础颜色"
|
||||
tooltip="物体基础颜色"
|
||||
>
|
||||
<ColorInput
|
||||
:model-value="colorToHex(materialData.albedoColor)"
|
||||
@update:model-value="updateAlbedoColor"
|
||||
/>
|
||||
</PropertyRow>
|
||||
|
||||
<!-- 贴图 -->
|
||||
<PropertyRow
|
||||
label="基础贴图"
|
||||
tooltip="颜色贴图"
|
||||
>
|
||||
<Field
|
||||
v-model="albedoTextureValue"
|
||||
accepted-type="Texture"
|
||||
@change="updateAlbedoTexture"
|
||||
/>
|
||||
</PropertyRow>
|
||||
|
||||
<!-- 透明度 -->
|
||||
<PropertyRow
|
||||
label="透明度"
|
||||
tooltip="材质透明度 (0-1)"
|
||||
>
|
||||
<Slider
|
||||
v-model="materialData.alpha"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
:show-value="true"
|
||||
@update:model-value="updateMaterialProperty('alpha', $event)"
|
||||
/>
|
||||
</PropertyRow>
|
||||
|
||||
|
||||
|
||||
<!-- 透明模式 -->
|
||||
<PropertyRow
|
||||
label="透明模式"
|
||||
tooltip="透明模式"
|
||||
>
|
||||
<Select
|
||||
v-model="materialData.transparencyMode"
|
||||
:options="transparencyModeOptions"
|
||||
@change="updateMaterialProperty('transparencyMode', $event)"
|
||||
/>
|
||||
</PropertyRow>
|
||||
</div>
|
||||
</CollapsibleHeader>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import CollapsibleHeader from 'components/public/CollapsibleHeader.vue'
|
||||
import PropertyRow from 'components/BasicControls/PropertyRow/index.vue'
|
||||
import Select from 'components/BasicControls/Select/index.vue'
|
||||
import ColorInput from 'components/BasicControls/Input/ColorInput.vue'
|
||||
import NumberInput from 'components/BasicControls/Input/index.vue'
|
||||
import Slider from 'components/BasicControls/Slider/index.vue'
|
||||
import Field from 'components/BasicControls/Field/index.vue'
|
||||
import { useInspectorPanel, DEFAULT_COMPONENT_CONFIGS, colorUtils } from '../../../composables/useInspectorPanel'
|
||||
import type {
|
||||
MaterialData,
|
||||
Color3,
|
||||
MaterialTypeOption,
|
||||
TransparencyModeOption
|
||||
} from './types'
|
||||
import type { FieldValue } from 'components/BasicControls/Field/types'
|
||||
import {contain} from "./index"
|
||||
import { useInspectorPanelStore } from 'stores/InspectorPanel'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const inspectorPanelStore = useInspectorPanelStore()
|
||||
const { selecteInspectorData } = storeToRefs(inspectorPanelStore)
|
||||
// 使用通用的 Inspector Panel 逻辑
|
||||
const {
|
||||
isExpanded,
|
||||
isEnabled,
|
||||
createEnabledChangeHandler,
|
||||
createActionClickHandler,
|
||||
createShowMoreHandler,
|
||||
createPropertyUpdater
|
||||
} = useInspectorPanel()
|
||||
|
||||
// Material 数据
|
||||
const materialData = reactive<MaterialData>({ ...DEFAULT_COMPONENT_CONFIGS.material })
|
||||
|
||||
// 贴图字段值
|
||||
const albedoTextureValue = ref<FieldValue | null>(null)
|
||||
|
||||
// 材质类型选项
|
||||
const materialTypeOptions: MaterialTypeOption[] = [
|
||||
{ label: 'PBR材质', value: 'PBRMaterial' }
|
||||
]
|
||||
|
||||
// 透明模式选项
|
||||
const transparencyModeOptions: TransparencyModeOption[] = [
|
||||
{ label: '不透明', value: 0 },
|
||||
{ label: '透明测试', value: 1 },
|
||||
{ label: '透明混合', value: 2 },
|
||||
{ label: '预乘透明混合', value: 3 }
|
||||
]
|
||||
|
||||
const visible = ref<any>({})
|
||||
const inspectorPanelData = ref<any>({})
|
||||
watch(selecteInspectorData, (node: any) => {
|
||||
|
||||
if (!node) {
|
||||
visible.value = false
|
||||
return;
|
||||
}
|
||||
|
||||
if(contain.includes(node.type)){
|
||||
visible.value = true
|
||||
}else{
|
||||
visible.value = false
|
||||
}
|
||||
inspectorPanelData.value = node
|
||||
})
|
||||
|
||||
|
||||
|
||||
// 更新 Material 属性
|
||||
const updateMaterialProperty = createPropertyUpdater(materialData, '材质', (property: keyof MaterialData, value: any) => {
|
||||
// 这里可以添加与 Babylon.js 的交互逻辑
|
||||
// 例如:updateSelectedMaterial(property, value)
|
||||
})
|
||||
|
||||
// 更新基础颜色
|
||||
const updateAlbedoColor = (hex: string) => {
|
||||
materialData.albedoColor = colorUtils.hexToRgb(hex)
|
||||
// console.log('基础颜色已更新:', materialData.albedoColor)
|
||||
|
||||
// 这里可以添加与 Babylon.js 的交互逻辑
|
||||
}
|
||||
|
||||
// 更新贴图
|
||||
const updateAlbedoTexture = (value: FieldValue | null) => {
|
||||
materialData.albedoTexture = value?.path || null
|
||||
// console.log('基础贴图已更新:', materialData.albedoTexture)
|
||||
|
||||
// 这里可以添加与 Babylon.js 的交互逻辑
|
||||
}
|
||||
|
||||
// 重置 Material 设置
|
||||
const resetMaterialSettings = () => {
|
||||
Object.assign(materialData, DEFAULT_COMPONENT_CONFIGS.material)
|
||||
albedoTextureValue.value = null
|
||||
// console.log('材质设置已重置')
|
||||
}
|
||||
|
||||
// 处理启用状态变化
|
||||
const handleEnabledChange = createEnabledChangeHandler('材质', (enabled: boolean) => {
|
||||
// 这里可以添加与 Babylon.js 的交互逻辑
|
||||
})
|
||||
|
||||
// 处理操作按钮点击
|
||||
const handleActionClick = createActionClickHandler(
|
||||
resetMaterialSettings,
|
||||
createShowMoreHandler('材质')
|
||||
)
|
||||
|
||||
// 颜色转换为十六进制
|
||||
const colorToHex = colorUtils.rgbToHex
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.material-content {
|
||||
/* Remove redundant styling since CollapsibleHeader handles container */
|
||||
}
|
||||
</style>
|
||||
36
src/components/InspectorPanel/Material/types.ts
Normal file
@ -0,0 +1,36 @@
|
||||
// Material 组件类型定义
|
||||
|
||||
export type MaterialType = 'PBRMaterial'
|
||||
|
||||
export interface Color3 {
|
||||
r: number
|
||||
g: number
|
||||
b: number
|
||||
}
|
||||
|
||||
export interface MaterialData {
|
||||
type: MaterialType
|
||||
albedoColor: Color3
|
||||
albedoTexture: string | null
|
||||
alpha: number
|
||||
transparencyMode: number
|
||||
}
|
||||
|
||||
export interface MaterialProps {
|
||||
// 可以添加其他 props
|
||||
}
|
||||
|
||||
export interface MaterialEmits {
|
||||
// 可以添加事件定义
|
||||
}
|
||||
|
||||
export interface MaterialTypeOption {
|
||||
label: string
|
||||
value: MaterialType
|
||||
}
|
||||
|
||||
// 透明模式选项
|
||||
export interface TransparencyModeOption {
|
||||
label: string
|
||||
value: number
|
||||
}
|
||||
1
src/components/InspectorPanel/Mesh/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export const contain=["Mesh"]
|
||||
134
src/components/InspectorPanel/Mesh/index.vue
Normal file
@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="mesh-panel" v-show="visible">
|
||||
<CollapsibleHeader title="网格"
|
||||
:expanded="isExpanded"
|
||||
:enabled="isEnabled"
|
||||
:checkable="false"
|
||||
@update:expanded="isExpanded = $event"
|
||||
@update:enabled="handleEnabledChange"
|
||||
@action-click="handleActionClick">
|
||||
<div class="mesh-properties">
|
||||
<!-- 是否可被鼠标拾取 -->
|
||||
<PropertyRow
|
||||
label="可拾取"
|
||||
tooltip="是否可被鼠标拾取"
|
||||
>
|
||||
<Switch
|
||||
v-model="meshData.isPickable"
|
||||
@change="updateMeshProperty('isPickable', $event)"
|
||||
/>
|
||||
</PropertyRow>
|
||||
|
||||
<!-- 是否参与碰撞检测 -->
|
||||
<PropertyRow
|
||||
label="碰撞检测"
|
||||
tooltip="是否参与碰撞检测"
|
||||
>
|
||||
<Switch
|
||||
v-model="meshData.checkCollisions"
|
||||
@change="updateMeshProperty('checkCollisions', $event)"
|
||||
/>
|
||||
</PropertyRow>
|
||||
<!-- 贴图 -->
|
||||
<PropertyRow
|
||||
label="材质"
|
||||
tooltip="材质"
|
||||
>
|
||||
<Field
|
||||
v-model="MaterialValue"
|
||||
accepted-type="Material"
|
||||
@change="updateAlbedoTexture"
|
||||
/>
|
||||
</PropertyRow>
|
||||
</div>
|
||||
</CollapsibleHeader>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import CollapsibleHeader from 'components/public/CollapsibleHeader.vue'
|
||||
import PropertyRow from 'components/BasicControls/PropertyRow/index.vue'
|
||||
import Switch from 'components/BasicControls/Switch/index.vue'
|
||||
import { useInspectorPanel, DEFAULT_COMPONENT_CONFIGS } from '../../../composables/useInspectorPanel'
|
||||
import type { MeshData } from './types'
|
||||
import {contain} from "./index"
|
||||
import { useInspectorPanelStore } from 'stores/InspectorPanel'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Field from 'components/BasicControls/Field/index.vue'
|
||||
import type { FieldValue } from 'components/BasicControls/Field/types'
|
||||
const inspectorPanelStore = useInspectorPanelStore()
|
||||
const { selecteInspectorData } = storeToRefs(inspectorPanelStore)
|
||||
// 使用通用的 Inspector Panel 逻辑
|
||||
const {
|
||||
isExpanded,
|
||||
isEnabled,
|
||||
createEnabledChangeHandler,
|
||||
createActionClickHandler,
|
||||
createShowMoreHandler,
|
||||
createPropertyUpdater
|
||||
} = useInspectorPanel()
|
||||
|
||||
|
||||
// Mesh 数据
|
||||
const meshData = reactive<MeshData>({ ...DEFAULT_COMPONENT_CONFIGS.mesh })
|
||||
|
||||
const visible = ref<any>({})
|
||||
const inspectorPanelData = ref<any>({})
|
||||
|
||||
// 贴图字段值
|
||||
const MaterialValue = ref<FieldValue | null>(null)
|
||||
|
||||
watch(selecteInspectorData, (node: any) => {
|
||||
if (!node) {
|
||||
|
||||
visible.value = false
|
||||
return;
|
||||
}
|
||||
|
||||
if(contain.includes(node.type)){
|
||||
visible.value = true
|
||||
|
||||
}else{
|
||||
visible.value = false
|
||||
}
|
||||
inspectorPanelData.value = node
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
// 更新 Mesh 属性
|
||||
const updateMeshProperty = createPropertyUpdater(meshData, '网格', (property: keyof MeshData, value: boolean) => {
|
||||
// 这里可以添加与 Babylon.js 的交互逻辑
|
||||
// 例如:updateSelectedMesh(property, value)
|
||||
})
|
||||
|
||||
// 重置 Mesh 设置
|
||||
const resetMeshSettings = () => {
|
||||
Object.assign(meshData, DEFAULT_COMPONENT_CONFIGS.mesh)
|
||||
// console.log('网格设置已重置')
|
||||
}
|
||||
|
||||
// 处理启用状态变化
|
||||
const handleEnabledChange = createEnabledChangeHandler('网格', (enabled: boolean) => {
|
||||
// 这里可以添加与 Babylon.js 的交互逻辑
|
||||
})
|
||||
|
||||
// 处理操作按钮点击
|
||||
const handleActionClick = createActionClickHandler(
|
||||
resetMeshSettings,
|
||||
createShowMoreHandler('网格')
|
||||
)
|
||||
|
||||
const updateAlbedoTexture = (value: FieldValue | null) => {
|
||||
console.log('更新基础贴图', value)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mesh-properties {
|
||||
/* Remove redundant styling since CollapsibleHeader handles container */
|
||||
}
|
||||
</style>
|
||||
14
src/components/InspectorPanel/Mesh/types.ts
Normal file
@ -0,0 +1,14 @@
|
||||
// Mesh 组件类型定义
|
||||
|
||||
export interface MeshData {
|
||||
isPickable: boolean
|
||||
checkCollisions: boolean
|
||||
}
|
||||
|
||||
export interface MeshProps {
|
||||
// 可以添加其他 props
|
||||
}
|
||||
|
||||
export interface MeshEmits {
|
||||
// 可以添加事件定义
|
||||
}
|
||||