添加一些数据

This commit is contained in:
yuding
2026-04-21 15:07:49 +08:00
parent d60cc4448d
commit aeb4c990ad
24 changed files with 39086 additions and 26411 deletions

9
demo-next/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
node_modules/
.next/
dist/
*.log
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

108
demo-next/README.md Normal file
View File

@@ -0,0 +1,108 @@
# iFlow Engine Next.js Demo
这是一个使用 Next.js 15 + React 19 + TypeScript 的演示项目,展示如何在现代 React 框架中集成 iFlow Engine BIM 引擎 SDK。
## 前置要求
1. 确保父项目已经构建完成:
```bash
cd ..
npm run build
```
2. 复制 SDK 文件到 public 目录:
```bash
npm run copy-sdk
```
3. 安装依赖:
```bash
npm install
```
## 运行
### 开发模式
```bash
npm run dev
```
服务器会在 `http://localhost:3000` 启动。
### 构建
```bash
npm run build
```
构建后的静态文件会在 `dist` 目录中。
### 预览构建结果
```bash
npm start
```
## 项目结构
```
demo-next/
├── app/ # Next.js App Router
│ ├── layout.tsx # 根布局
│ ├── page.tsx # 主页面
│ └── globals.css # 全局样式
├── components/ # React 组件
│ ├── BimViewer.tsx # BIM 引擎封装组件
│ └── ControlPanel.tsx # 控制面板组件
├── public/ # 静态资源
│ ├── lib/ # SDK 文件目录
│ │ ├── iflow-engine.umd.js
│ │ ├── iflow-engine.umd.js.map
│ │ └── iflow-engine.es.js
│ └── model/ # 3D 模型文件
├── package.json # 项目配置
├── tsconfig.json # TypeScript 配置
├── next.config.js # Next.js 配置
└── README.md # 说明文档
```
## 核心功能
- 🌍 **语言切换**: 支持中文/英文切换
- 🪟 **弹窗测试**: 测试对话框组件
- 🛠️ **工具栏控制**: 显示/隐藏工具栏和标签
- 🎨 **主题切换**: 深色/浅色/自定义主题
- 🎮 **3D 引擎**: 初始化引擎、加载模型、暂停/恢复渲染
## 使用说明
1. 点击 "初始化引擎" 按钮初始化 3D 引擎
2. 点击 "加载模型" 按钮加载默认 3D 模型(或输入自定义 URL
3. 其他功能按钮可以测试 SDK 的各种功能
## 技术栈
- Next.js 15.3
- React 19.1
- TypeScript 5.9
## 注意事项
- 确保父项目的 `dist` 目录中有构建好的 SDK 文件
- 使用 HTTP 服务器运行(不能直接打开 HTML 文件)以避免 CORS 问题
- 页面使用客户端组件('use client')因为 BIM 引擎需要浏览器环境
## 与 demo-vue 的区别
| 特性 | demo-vue | demo-next |
|------|----------|-----------|
| 框架 | Vue 3 | Next.js 15 + React 19 |
| 路由 | Vue Router | Next.js App Router |
| 样式 | Scoped CSS | CSS-in-JSX |
| 构建输出 | SPA | 静态导出 |
| TypeScript | 可选 | 默认 |
## 许可证
ISC

13
demo-next/app/globals.css Normal file
View File

@@ -0,0 +1,13 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}

22
demo-next/app/layout.tsx Normal file
View File

@@ -0,0 +1,22 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "iFlow Engine - Next.js Demo",
description: "iFlow Engine BIM Viewer Demo with Next.js + React",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<head>
<script src="/lib/iflow-engine.umd.js" defer></script>
</head>
<body>{children}</body>
</html>
);
}

9
demo-next/app/page.tsx Normal file
View File

@@ -0,0 +1,9 @@
import BimViewer from "@/components/BimViewer";
export default function Home() {
return (
<main style={{ width: "100vw", height: "100vh", overflow: "hidden" }}>
<BimViewer />
</main>
);
}

View File

@@ -0,0 +1,262 @@
'use client';
import { useEffect, useRef, useState, useCallback } from 'react';
import ControlPanel from './ControlPanel';
declare global {
interface Window {
IflowEngine: any;
}
}
const DEFAULT_MODEL_URL = 'https://lyz-1259524260.cos.ap-guangzhou.myqcloud.com/iflow/models/8634e556-a94e-4ba7-be3e-2ea1507cced5/';
export default function BimViewer() {
const containerRef = useRef<HTMLDivElement>(null);
const engineRef = useRef<any>(null);
const [engineStatus, setEngineStatus] = useState<string>('未初始化');
const [isLoading, setIsLoading] = useState(false);
const updateStatus = useCallback((status: string) => {
setEngineStatus(status);
}, []);
useEffect(() => {
if (!containerRef.current || typeof window === 'undefined') return;
if (!window.IflowEngine) {
console.error('SDK not found');
updateStatus('SDK 未加载');
return;
}
const Engine = window.IflowEngine.BimEngine;
const engine = new Engine(containerRef.current, { locale: 'zh-CN' });
engineRef.current = engine;
engine.rightKey.registerHandler((event: any) => {
const { x, y } = event;
return [
{
id: 'home',
label: 'toolbar.home',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>',
onClick: () => {
console.log('Go Home');
}
},
{
id: 'log-pos',
label: '打印坐标',
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>',
onClick: () => {
console.log(`Clicked at: ${x}, ${y}`);
}
}
];
});
return () => {
if (engineRef.current) {
engineRef.current.destroy();
engineRef.current = null;
}
};
}, [updateStatus]);
const initEngine = useCallback(() => {
const engine = engineRef.current;
if (!engine || !engine.engine) {
alert('引擎未创建,请先等待页面加载完成');
return;
}
if (engine.engine.isInitialized()) {
alert('3D 引擎已经初始化过了');
updateStatus('已初始化');
return;
}
try {
const success = engine.engine.initialize({
backgroundColor: 0x333333,
version: 'v2',
showStats: false,
showViewCube: true
});
if (success) {
updateStatus('已初始化');
console.log('3D 引擎初始化成功');
} else {
updateStatus('初始化失败');
console.error('3D 引擎初始化失败');
}
} catch (error: any) {
updateStatus('初始化错误');
console.error('3D 引擎初始化错误:', error);
}
}, [updateStatus]);
const loadModel = useCallback((url?: string) => {
const engine = engineRef.current;
if (!engine || !engine.engine) {
alert('引擎未创建');
return;
}
if (!engine.engine.isInitialized()) {
alert('请先初始化 3D 引擎!');
return;
}
const modelUrl = url || DEFAULT_MODEL_URL;
if (!modelUrl.trim()) {
alert('请输入模型 URL');
return;
}
setIsLoading(true);
try {
engine.engine.loadModel([modelUrl], {
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1]
});
console.log('模型加载请求已发送:', modelUrl);
} catch (error: any) {
console.error('模型加载错误:', error);
} finally {
setTimeout(() => setIsLoading(false), 2000);
}
}, []);
const setLang = useCallback((lang: 'zh-CN' | 'en-US') => {
if (engineRef.current) {
engineRef.current.setLocale(lang);
}
}, []);
const setTheme = useCallback((themeName: 'dark' | 'light') => {
if (engineRef.current) {
engineRef.current.setTheme(themeName);
}
}, []);
const setCustomTheme = useCallback(() => {
if (!engineRef.current) return;
engineRef.current.setCustomTheme({
name: 'red-alert',
primary: '#d32f2f',
primaryHover: '#b71c1c',
background: '#ffebee',
panelBackground: 'rgba(255, 255, 255, 0.9)',
textPrimary: '#b71c1c',
textSecondary: '#e57373',
border: '#ffcdd2',
icon: '#d32f2f',
iconActive: '#b71c1c',
componentBackground: 'rgba(255, 205, 210, 0.3)',
componentHover: 'rgba(255, 205, 210, 0.8)',
componentActive: '#e57373'
});
}, []);
const openTestDialog = useCallback(() => {
if (!engineRef.current || !engineRef.current.dialog) return;
engineRef.current.dialog.create({
title: 'dialog.testTitle',
width: 350,
height: 200,
position: 'center',
draggable: true,
resizable: true
});
}, []);
const openInfoDialog = useCallback(() => {
if (!engineRef.current || !engineRef.current.dialog) return;
engineRef.current.dialog.showInfoDialog();
}, []);
const openTreeDialog = useCallback(() => {
if (!engineRef.current || !engineRef.current.modelTree) return;
engineRef.current.modelTree.showStructTree();
}, []);
const toggleToolbar = useCallback((visible: boolean) => {
if (!engineRef.current || !engineRef.current.toolbar) return;
engineRef.current.toolbar.setVisible(visible);
}, []);
const toggleLabel = useCallback((visible: boolean) => {
if (!engineRef.current || !engineRef.current.toolbar) return;
engineRef.current.toolbar.setShowLabel(visible);
}, []);
const pauseRendering = useCallback(() => {
if (!engineRef.current?.engine?.isInitialized()) {
alert('请先初始化 3D 引擎!');
return;
}
engineRef.current.engine.pauseRendering();
console.log('渲染已暂停');
}, []);
const resumeRendering = useCallback(() => {
if (!engineRef.current?.engine?.isInitialized()) {
alert('请先初始化 3D 引擎!');
return;
}
engineRef.current.engine.resumeRendering();
console.log('渲染已恢复');
}, []);
return (
<div style={{ display: 'flex', width: '100%', height: '100%', overflow: 'hidden' }}>
<ControlPanel
engineStatus={engineStatus}
isLoading={isLoading}
onInitEngine={initEngine}
onLoadModel={loadModel}
onSetLang={setLang}
onSetTheme={setTheme}
onSetCustomTheme={setCustomTheme}
onOpenTestDialog={openTestDialog}
onOpenInfoDialog={openInfoDialog}
onOpenTreeDialog={openTreeDialog}
onToggleToolbar={toggleToolbar}
onToggleLabel={toggleLabel}
onPauseRendering={pauseRendering}
onResumeRendering={resumeRendering}
/>
<div
ref={containerRef}
style={{
flex: 1,
position: 'relative',
background: '#1a1a1a',
overflow: 'hidden'
}}
/>
{isLoading && (
<div
style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: 'rgba(0, 0, 0, 0.8)',
color: '#fff',
padding: '20px 40px',
borderRadius: '8px',
zIndex: 9999,
fontSize: '16px'
}}
>
...
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,224 @@
'use client';
import { useState } from 'react';
interface ControlPanelProps {
engineStatus: string;
isLoading: boolean;
onInitEngine: () => void;
onLoadModel: (url?: string) => void;
onSetLang: (lang: 'zh-CN' | 'en-US') => void;
onSetTheme: (theme: 'dark' | 'light') => void;
onSetCustomTheme: () => void;
onOpenTestDialog: () => void;
onOpenInfoDialog: () => void;
onOpenTreeDialog: () => void;
onToggleToolbar: (visible: boolean) => void;
onToggleLabel: (visible: boolean) => void;
onPauseRendering: () => void;
onResumeRendering: () => void;
}
export default function ControlPanel({
engineStatus,
onInitEngine,
onLoadModel,
onSetLang,
onSetTheme,
onSetCustomTheme,
onOpenTestDialog,
onOpenInfoDialog,
onOpenTreeDialog,
onToggleToolbar,
onToggleLabel,
onPauseRendering,
onResumeRendering
}: ControlPanelProps) {
const [modelUrl, setModelUrl] = useState('');
const [isToolbarVisible, setIsToolbarVisible] = useState(true);
const [isLabelVisible, setIsLabelVisible] = useState(true);
const handleToggleToolbar = () => {
const newVisible = !isToolbarVisible;
setIsToolbarVisible(newVisible);
onToggleToolbar(newVisible);
};
const handleToggleLabel = () => {
const newVisible = !isLabelVisible;
setIsLabelVisible(newVisible);
onToggleLabel(newVisible);
};
const handleLoadModel = () => {
const url = modelUrl.trim();
onLoadModel(url || undefined);
};
const getStatusColor = () => {
switch (engineStatus) {
case '已初始化':
return '#28a745';
case '初始化失败':
case '初始化错误':
case 'SDK 未加载':
return '#dc3545';
default:
return '#666';
}
};
return (
<aside
style={{
width: '320px',
background: '#fff',
borderRight: '1px solid #e0e0e0',
padding: '20px',
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: '20px',
boxShadow: '2px 0 5px rgba(0, 0, 0, 0.05)',
zIndex: 10
}}
>
<h1 style={{ fontSize: '1.4rem', color: '#333', marginBottom: '10px' }}>
iFlow Engine Demo
</h1>
<p style={{ fontSize: '0.85rem', color: '#666', marginBottom: '10px' }}>
Next.js + React
</p>
<ControlGroup title="🌍 语言 (Language)">
<ButtonGroup>
<button className="btn-primary" onClick={() => onSetLang('zh-CN')}></button>
<button className="btn-primary" onClick={() => onSetLang('en-US')}>English</button>
</ButtonGroup>
</ControlGroup>
<ControlGroup title="🪟 弹窗 (Dialog)">
<ButtonGroup>
<button onClick={onOpenTestDialog}></button>
<button onClick={onOpenInfoDialog}></button>
<button onClick={onOpenTreeDialog}></button>
</ButtonGroup>
</ControlGroup>
<ControlGroup title="🛠️ 工具栏 (Toolbar)">
<ButtonGroup>
<button onClick={handleToggleToolbar}>
{isToolbarVisible ? '隐藏工具栏' : '显示工具栏'}
</button>
<button onClick={handleToggleLabel}>
{isLabelVisible ? '隐藏标签' : '显示标签'}
</button>
</ButtonGroup>
</ControlGroup>
<ControlGroup title="🎨 样式 (Theme)">
<ButtonGroup>
<button onClick={() => onSetTheme('dark')}></button>
<button onClick={() => onSetTheme('light')}></button>
<button onClick={onSetCustomTheme}></button>
</ButtonGroup>
</ControlGroup>
<ControlGroup title="🎮 3D 引擎 (Engine)">
<ButtonGroup>
<button className="btn-primary" onClick={onInitEngine}></button>
<button className="btn-primary" onClick={handleLoadModel}></button>
</ButtonGroup>
<input
type="text"
value={modelUrl}
onChange={(e) => setModelUrl(e.target.value)}
placeholder="输入模型 URL可选"
style={{
width: '100%',
padding: '6px 8px',
fontSize: '0.85rem',
border: '1px solid #ddd',
borderRadius: '4px',
fontFamily: 'Consolas, Monaco, monospace',
outline: 'none',
marginTop: '8px'
}}
/>
<ButtonGroup style={{ marginTop: '8px' }}>
<button onClick={onPauseRendering}></button>
<button onClick={onResumeRendering}></button>
</ButtonGroup>
<div style={{ marginTop: '10px', fontSize: '0.85rem', color: '#666' }}>
: <span style={{ color: getStatusColor() }}>{engineStatus}</span>
</div>
</ControlGroup>
<style jsx>{`
button {
padding: 6px 12px;
font-size: 0.9rem;
border: 1px solid #ddd;
background: #fff;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
flex: 1 1 auto;
}
button:hover {
background: #f0f0f0;
border-color: #ccc;
}
.btn-primary {
background: #0078d4;
color: white;
border-color: #0063b1;
}
.btn-primary:hover {
background: #0063b1;
}
`}</style>
</aside>
);
}
function ControlGroup({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div
style={{
background: '#f9f9f9',
padding: '15px',
borderRadius: '8px',
border: '1px solid #eee'
}}
>
<h2
style={{
fontSize: '1rem',
color: '#555',
marginBottom: '10px',
paddingBottom: '5px',
borderBottom: '2px solid #e0e0e0'
}}
>
{title}
</h2>
{children}
</div>
);
}
function ButtonGroup({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) {
return (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '8px',
...style
}}
>
{children}
</div>
);
}

6
demo-next/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

9
demo-next/next.config.js Normal file
View File

@@ -0,0 +1,9 @@
const nextConfig = {
output: 'export',
distDir: 'dist',
images: {
unoptimized: true
}
};
module.exports = nextConfig;

2464
demo-next/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
demo-next/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "iflow-engine-next-demo",
"version": "1.0.0",
"description": "iFlow Engine Next.js + React Demo",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"copy-sdk": "mkdir -p public/lib && cp ../dist/iflow-engine.umd.js public/lib/ && cp ../dist/iflow-engine.umd.js.map public/lib/ && cp ../dist/iflow-engine.es.js public/lib/"
},
"dependencies": {
"iflow-engine": "2.7.2",
"next": "^15.3.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@types/node": "^22.14.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"typescript": "^5.9.3"
}
}

40
demo-next/tsconfig.json Normal file
View File

@@ -0,0 +1,40 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
},
"target": "ES2017"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}