1
This commit is contained in:
@ -1,933 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>3D Model Showcase SDK - TS</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
overflow: hidden;
|
|
||||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#canvas-container {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#renderDom {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-panel {
|
|
||||||
width: 320px;
|
|
||||||
background: rgba(30, 30, 45, 0.95);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-panel::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-panel::-webkit-scrollbar-track {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-panel::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-title {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-info {
|
|
||||||
background: rgba(76, 175, 80, 0.2);
|
|
||||||
border: 1px solid rgba(76, 175, 80, 0.5);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-info-title {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #4caf50;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-info-item {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-info-label {
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
min-width: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-info-value {
|
|
||||||
color: #fff;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-category {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header {
|
|
||||||
padding: 12px 15px;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
transition: background 0.3s;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header.active {
|
|
||||||
background: rgba(76, 175, 80, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-arrow {
|
|
||||||
transition: transform 0.3s;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-arrow.expanded {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-content {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 0fr;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: grid-template-rows 0.3s ease, padding 0.3s ease;
|
|
||||||
padding: 0 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-content.expanded {
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-content>* {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-item {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-label {
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-group {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-btn:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border-color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-btn.selected {
|
|
||||||
background: rgba(76, 175, 80, 0.6);
|
|
||||||
border-color: #4CAF50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-checkbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-checkbox:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-checkbox input[type="checkbox"] {
|
|
||||||
margin-right: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-checkbox label {
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 进度条样式 */
|
|
||||||
#progress-container {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 80%;
|
|
||||||
max-width: 500px;
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress-bar {
|
|
||||||
width: 0%;
|
|
||||||
height: 10px;
|
|
||||||
background: linear-gradient(90deg, #4CAF50, #45a049);
|
|
||||||
border-radius: 5px;
|
|
||||||
transition: width 0.1s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress-text {
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 5px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<!-- 画布区域 -->
|
|
||||||
<div id="canvas-container">
|
|
||||||
<canvas id="renderDom"></canvas>
|
|
||||||
<div id="progress-container" style="display: none;">
|
|
||||||
<div id="progress-bar"></div>
|
|
||||||
<div id="progress-text">0%</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 生成放置区域按钮 -->
|
|
||||||
<button id="dropzone-btn" style="
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
left: 20px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: #21c7ff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
||||||
z-index: 100;
|
|
||||||
">生成放置区域</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 配置面板 -->
|
|
||||||
<div id="config-panel">
|
|
||||||
<div class="config-title">选装选配</div>
|
|
||||||
|
|
||||||
<!-- 点击信息显示区域 -->
|
|
||||||
<div id="click-info" class="click-info" style="display: none;">
|
|
||||||
<div class="click-info-title">点击信息</div>
|
|
||||||
<div id="click-info-content"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 棚子尺寸 -->
|
|
||||||
<div class="config-category">
|
|
||||||
<div class="category-header" data-category="size">
|
|
||||||
<span class="category-title">棚子尺寸</span>
|
|
||||||
<span class="category-arrow">▼</span>
|
|
||||||
</div>
|
|
||||||
<div class="category-content">
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="option-btn" data-option="size-1">3x3米</button>
|
|
||||||
<button class="option-btn" data-option="size-2">4x4米</button>
|
|
||||||
<button class="option-btn" data-option="size-3">5x5米</button>
|
|
||||||
<button class="option-btn" data-option="size-4">6x6米</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 棚子类型 -->
|
|
||||||
<div class="config-category">
|
|
||||||
<div class="category-header" data-category="type">
|
|
||||||
<span class="category-title">棚子类型</span>
|
|
||||||
<span class="category-arrow">▼</span>
|
|
||||||
</div>
|
|
||||||
<div class="category-content">
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="option-btn" data-option="type-1">平顶</button>
|
|
||||||
<button class="option-btn" data-option="type-2">尖顶</button>
|
|
||||||
<button class="option-btn" data-option="type-3">弧形</button>
|
|
||||||
<button class="option-btn" data-option="type-4">异形</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 百叶 (单选) -->
|
|
||||||
<div class="config-category">
|
|
||||||
<div class="category-header" data-category="louver">
|
|
||||||
<span class="category-title">百叶</span>
|
|
||||||
<span class="category-arrow">▼</span>
|
|
||||||
</div>
|
|
||||||
<div class="category-content">
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="option-btn" data-option="louver-1">整体</button>
|
|
||||||
<button class="option-btn" data-option="louver-2">3m百叶</button>
|
|
||||||
<button class="option-btn" data-option="louver-3">3m下拉帘</button>
|
|
||||||
<button class="option-btn" data-option="louver-4">百叶4</button>
|
|
||||||
<button class="option-btn" data-option="louver-4">卷帘小</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 配色 -->
|
|
||||||
<div class="config-category">
|
|
||||||
<div class="category-header" data-category="color">
|
|
||||||
<span class="category-title">配色</span>
|
|
||||||
<span class="category-arrow">▼</span>
|
|
||||||
</div>
|
|
||||||
<div class="category-content">
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="option-btn" data-option="color-1">222222</button>
|
|
||||||
<button class="option-btn" data-option="color-2">灰色</button>
|
|
||||||
<button class="option-btn" data-option="color-3">黑色</button>
|
|
||||||
<button class="option-btn" data-option="color-4">木色</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="hotspot-btn">生成热点</button>
|
|
||||||
<button id="prevent-btn">生成防止区域</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 模型信息框(用于2D转3D显示) -->
|
|
||||||
<div id="model-info-box" style="display: none;">
|
|
||||||
<div style="
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
padding: 15px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
||||||
font-size: 14px;
|
|
||||||
color: #333;
|
|
||||||
min-width: 200px;
|
|
||||||
">
|
|
||||||
<div style="font-weight: bold; margin-bottom: 8px; color: #4CAF50;">模型信息</div>
|
|
||||||
<div id="info-name" style="margin-bottom: 5px;">名称: -</div>
|
|
||||||
<div id="info-position" style="margin-bottom: 10px; font-size: 12px; color: #666;">坐标: -</div>
|
|
||||||
|
|
||||||
<!-- 颜色按钮 -->
|
|
||||||
<div id="color-buttons" style="display: none; gap: 8px; margin-bottom: 8px;">
|
|
||||||
<button id="color-btn-1" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #FFFFFF;
|
|
||||||
color: #333;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">白色</button>
|
|
||||||
<button id="color-btn-2" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #000000;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">黑色</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 旋转按钮 -->
|
|
||||||
<div id="rotation-buttons" style="display: none; gap: 8px; margin-bottom: 8px;">
|
|
||||||
<button id="rotation-btn-90" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #2196F3;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">旋转90°</button>
|
|
||||||
<button id="rotation-btn-180" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #2196F3;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">旋转180°</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 8px;">
|
|
||||||
<button id="remove-model-btn" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #f44336;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">移除</button>
|
|
||||||
<button id="close-info-btn" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #4CAF50;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">关闭</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="module" src="./index.js"></script>
|
|
||||||
<script type="module">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { kernel } from './src/main.ts';
|
|
||||||
|
|
||||||
// ========== UI 交互逻辑 ==========
|
|
||||||
|
|
||||||
// 折叠面板切换
|
|
||||||
document.querySelectorAll('.category-header').forEach(header => {
|
|
||||||
header.addEventListener('click', function () {
|
|
||||||
const content = this.nextElementSibling;
|
|
||||||
const arrow = this.querySelector('.category-arrow');
|
|
||||||
|
|
||||||
// 切换展开/收起状态
|
|
||||||
content.classList.toggle('expanded');
|
|
||||||
arrow.classList.toggle('expanded');
|
|
||||||
this.classList.toggle('active');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
let sku = ""
|
|
||||||
// 单选按钮逻辑
|
|
||||||
document.querySelectorAll('.option-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', async function () {
|
|
||||||
const optionGroup = this.parentElement;
|
|
||||||
const category = this.closest('.config-category');
|
|
||||||
const categoryName = category.querySelector('.category-header').dataset.category;
|
|
||||||
|
|
||||||
// 同一组内取消其他选中状态
|
|
||||||
optionGroup.querySelectorAll('.option-btn').forEach(b => {
|
|
||||||
b.classList.remove('selected');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 选中当前按钮
|
|
||||||
this.classList.add('selected');
|
|
||||||
|
|
||||||
// 触发自定义事件
|
|
||||||
const event = new CustomEvent('config:change', {
|
|
||||||
detail: {
|
|
||||||
category: categoryName,
|
|
||||||
value: this.dataset.option,
|
|
||||||
text: this.textContent
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
|
|
||||||
console.log('配置变更:', {
|
|
||||||
category: categoryName,
|
|
||||||
value: this.dataset.option,
|
|
||||||
text: this.textContent
|
|
||||||
});
|
|
||||||
|
|
||||||
// 百叶模型替换逻辑
|
|
||||||
// if (categoryName === "louver") {
|
|
||||||
const currentText = this.textContent;
|
|
||||||
const response = await fetch(`http://localhost:3001/api/product-configs/by-sku/${currentText}`);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.code === 200) {
|
|
||||||
const { divisions, enable_placement_zone } = result.data;
|
|
||||||
// const {position_x, position_y, position_z} = data;
|
|
||||||
if (enable_placement_zone) {
|
|
||||||
sku = currentText;
|
|
||||||
await placementWall(divisions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// skuToFunc(currentText);
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
document.querySelector('#hotspot-btn').addEventListener('click', async function () {
|
|
||||||
|
|
||||||
await hotspotRequest();
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const hotspotRequest = async () => {
|
|
||||||
try {
|
|
||||||
// 从后端获取激活状态的热点列表
|
|
||||||
const response = await fetch('http://localhost:3001/api/hotspots?status=active&page=1&pageSize=100');
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.code === 200 && result.data.list.length > 0) {
|
|
||||||
// 将后端数据转换为 SDK 需要的格式
|
|
||||||
const hotspots = result.data.list.map(item => ({
|
|
||||||
id: item.id,
|
|
||||||
type: 'hotspot',
|
|
||||||
name: item.name,
|
|
||||||
meshName: item.name, // 可以根据实际情况调整
|
|
||||||
icon: item.image_url,
|
|
||||||
position: [item.position_x, item.position_y, item.position_z],
|
|
||||||
radius: item.radius,
|
|
||||||
color: "#000000",
|
|
||||||
payload: {
|
|
||||||
skus: item.skus || [],
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 渲染热点
|
|
||||||
kernel.hotspot.render(hotspots);
|
|
||||||
console.log('热点渲染成功:', hotspots);
|
|
||||||
} else {
|
|
||||||
console.log('没有可用的热点数据');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取热点数据失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听热点点击事件
|
|
||||||
window.addEventListener('hotspot:click', (event) => {
|
|
||||||
console.log('热点被点击:', event.detail);
|
|
||||||
const { id, name, payload } = event.detail;
|
|
||||||
|
|
||||||
const clickInfoDiv = document.getElementById('click-info');
|
|
||||||
const clickInfoContent = document.getElementById('click-info-content');
|
|
||||||
|
|
||||||
let html = `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">类型:</span>
|
|
||||||
<span class="click-info-value">热点</span>
|
|
||||||
</div>
|
|
||||||
<div class="click-info-item">
|
|
||||||
<span class="click-info-label">名称:</span>
|
|
||||||
<span class="click-info-value">${name}</span>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
if (payload && payload.skus && payload.skus.length > 0) {
|
|
||||||
html += `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">关联SKU:</span>
|
|
||||||
<span class="click-info-value">${payload.skus.join(', ')}</span>
|
|
||||||
</div>`;
|
|
||||||
} else {
|
|
||||||
html += `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">关联SKU:</span>
|
|
||||||
<span class="click-info-value">无</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
clickInfoContent.innerHTML = html;
|
|
||||||
clickInfoDiv.style.display = 'block';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听模型点击事件
|
|
||||||
window.addEventListener('model:click', (event) => {
|
|
||||||
console.log('模型被点击:', event.detail);
|
|
||||||
const { meshName, materialName, modelControlType } = event.detail;
|
|
||||||
|
|
||||||
const clickInfoDiv = document.getElementById('click-info');
|
|
||||||
const clickInfoContent = document.getElementById('click-info-content');
|
|
||||||
|
|
||||||
let html = `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">类型:</span>
|
|
||||||
<span class="click-info-value">模型</span>
|
|
||||||
</div>
|
|
||||||
<div class="click-info-item">
|
|
||||||
<span class="click-info-label">网格名称:</span>
|
|
||||||
<span class="click-info-value">${meshName}</span>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
if (materialName) {
|
|
||||||
html += `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">材质名称:</span>
|
|
||||||
<span class="click-info-value">${materialName}</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modelControlType) {
|
|
||||||
html += `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">控制类型:</span>
|
|
||||||
<span class="click-info-value">${modelControlType}</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
clickInfoContent.innerHTML = html;
|
|
||||||
clickInfoDiv.style.display = 'block';
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// 多选复选框逻辑
|
|
||||||
document.querySelectorAll('.option-checkbox input[type="checkbox"]').forEach(checkbox => {
|
|
||||||
checkbox.addEventListener('change', async function () {
|
|
||||||
const category = this.closest('.config-category');
|
|
||||||
const categoryName = category.querySelector('.category-header').dataset.category;
|
|
||||||
const optionGroup = this.closest('.option-group');
|
|
||||||
const checked = this.checked;
|
|
||||||
|
|
||||||
// 获取当前组所有选中的值
|
|
||||||
const selectedValues = Array.from(
|
|
||||||
optionGroup.querySelectorAll('input[type="checkbox"]:checked')
|
|
||||||
).map(cb => ({
|
|
||||||
value: cb.dataset.option,
|
|
||||||
text: cb.nextElementSibling.textContent
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 触发自定义事件
|
|
||||||
const event = new CustomEvent('config:change', {
|
|
||||||
detail: {
|
|
||||||
category: categoryName,
|
|
||||||
values: selectedValues,
|
|
||||||
checked: this.checked,
|
|
||||||
currentValue: this.dataset.option
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
|
|
||||||
console.log('配置变更(多选):', {
|
|
||||||
category: categoryName,
|
|
||||||
selectedValues: selectedValues,
|
|
||||||
checked: this.checked,
|
|
||||||
currentValue: this.dataset.option
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听配置变更事件(供外部使用)
|
|
||||||
document.addEventListener('config:change', function (e) {
|
|
||||||
// 这里可以根据配置变更来操作 3D 模型
|
|
||||||
// 例如:
|
|
||||||
// if (e.detail.category === 'size') {
|
|
||||||
// kernel.model.replace({ modelId: 'shed', modelUrl: `/models/shed-${e.detail.value}.glb`, modelControlType: 'rotation' });
|
|
||||||
// }
|
|
||||||
// if (e.detail.category === 'color') {
|
|
||||||
// kernel.material.apply({
|
|
||||||
// target: 'ShedMaterial',
|
|
||||||
// attribute: 'baseColor',
|
|
||||||
// value: getColorValue(e.detail.value)
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========== 模型信息框按钮事件 ==========
|
|
||||||
|
|
||||||
// 关闭按钮事件
|
|
||||||
document.getElementById('close-info-btn').addEventListener('click', () => {
|
|
||||||
kernel.domTo3D.detach('model-info');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 白色按钮事件
|
|
||||||
document.getElementById('color-btn-1').addEventListener('click', () => {
|
|
||||||
const materialName = window.getCurrentMaterialName();
|
|
||||||
if (materialName) {
|
|
||||||
console.log('切换为白色,材质名:', materialName);
|
|
||||||
kernel.material.apply({
|
|
||||||
target: materialName,
|
|
||||||
albedoColor: '#FFFFFF',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('没有选中材质');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 黑色按钮事件
|
|
||||||
document.getElementById('color-btn-2').addEventListener('click', () => {
|
|
||||||
const materialName = window.getCurrentMaterialName();
|
|
||||||
if (materialName) {
|
|
||||||
console.log('切换为黑色,材质名:', materialName);
|
|
||||||
kernel.material.apply({
|
|
||||||
target: materialName,
|
|
||||||
albedoColor: '#000000',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('没有选中材质');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 旋转90度按钮事件
|
|
||||||
document.getElementById('rotation-btn-90').addEventListener('click', () => {
|
|
||||||
const pickedMesh = window.getCurrentPickedMesh();
|
|
||||||
if (pickedMesh) {
|
|
||||||
const modelName = kernel.model.findModelNameByMesh?.(pickedMesh);
|
|
||||||
if (modelName) {
|
|
||||||
console.log('旋转90度,模型名:', modelName);
|
|
||||||
kernel.transform.rotation({
|
|
||||||
modelId: modelName,
|
|
||||||
vector3: { x: 0, y: 90, z: 0 }
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('未找到模型名称');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('没有选中的网格');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 旋转180度按钮事件
|
|
||||||
document.getElementById('rotation-btn-180').addEventListener('click', () => {
|
|
||||||
const pickedMesh = window.getCurrentPickedMesh();
|
|
||||||
if (pickedMesh) {
|
|
||||||
const modelName = kernel.model.findModelNameByMesh?.(pickedMesh);
|
|
||||||
if (modelName) {
|
|
||||||
console.log('旋转180度,模型名:', modelName);
|
|
||||||
kernel.transform.rotation({
|
|
||||||
modelId: modelName,
|
|
||||||
vector3: { x: 0, y: 30, z: 0 }
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('未找到模型名称');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('没有选中的网格');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 移除按钮事件
|
|
||||||
document.getElementById('remove-model-btn').addEventListener('click', () => {
|
|
||||||
const pickedMesh = window.getCurrentPickedMesh();
|
|
||||||
if (pickedMesh) {
|
|
||||||
const meshName = pickedMesh.name;
|
|
||||||
const success = kernel.model.remove(meshName);
|
|
||||||
if (success) {
|
|
||||||
console.log('模型已移除');
|
|
||||||
// 关闭信息框
|
|
||||||
kernel.domTo3D.detach('model-info');
|
|
||||||
} else {
|
|
||||||
console.log('移除失败:未找到该网格所属的模型');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('没有选中的网格');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 生成放置区域按钮事件
|
|
||||||
let dropZoneVisible = false;
|
|
||||||
document.getElementById('dropzone-btn').addEventListener('click', () => {
|
|
||||||
if (!dropZoneVisible) {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 更新按钮文字
|
|
||||||
document.getElementById('dropzone-btn').textContent = '隐藏放置区域';
|
|
||||||
console.log('已生成并显示放置区域');
|
|
||||||
} else {
|
|
||||||
// 隐藏放置区域
|
|
||||||
kernel.dropZone.hideAll();
|
|
||||||
dropZoneVisible = false;
|
|
||||||
|
|
||||||
// 更新按钮文字
|
|
||||||
document.getElementById('dropzone-btn').textContent = '生成放置区域';
|
|
||||||
console.log('已隐藏放置区域');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const placementWall = (divisions) => {
|
|
||||||
|
|
||||||
// 只清除旧的放置区域网格,不清除模型
|
|
||||||
kernel.dropZone.clearZones();
|
|
||||||
|
|
||||||
// 生成新的放置区域(使用新的墙面参数化API)
|
|
||||||
// 调整 baseY 来控制整体高度(正数向上,负数向下)
|
|
||||||
const baseY = 0.08; // 修改这个值来调整整体高度
|
|
||||||
|
|
||||||
// 调整 offset 来控制每个面向外或向内的偏移
|
|
||||||
// 正数 = 向外移动,负数 = 向内移动
|
|
||||||
const wallOffset = -0.07; // 修改这个值来调整墙面偏移
|
|
||||||
|
|
||||||
kernel.dropZone.generate({
|
|
||||||
walls: [
|
|
||||||
{
|
|
||||||
name: 'front',
|
|
||||||
startPoint: [-1.43, baseY, -1.4],
|
|
||||||
endPoint: [1.37, baseY, -1.4],
|
|
||||||
height: 2.2,
|
|
||||||
divisions: divisions,
|
|
||||||
offset: wallOffset // 向外或向内偏移
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'back',
|
|
||||||
startPoint: [1.37, baseY, 1.4],
|
|
||||||
endPoint: [-1.43, baseY, 1.4],
|
|
||||||
height: 2.2,
|
|
||||||
divisions: divisions,
|
|
||||||
offset: wallOffset
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'left',
|
|
||||||
startPoint: [-1.43, baseY, 1.4],
|
|
||||||
endPoint: [-1.43, baseY, -1.43],
|
|
||||||
height: 2.2,
|
|
||||||
divisions: divisions,
|
|
||||||
offset: wallOffset
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'right',
|
|
||||||
startPoint: [1.37, baseY, -1.43],
|
|
||||||
endPoint: [1.37, baseY, 1.4],
|
|
||||||
height: 2.2,
|
|
||||||
divisions: divisions,
|
|
||||||
offset: wallOffset
|
|
||||||
}
|
|
||||||
],
|
|
||||||
color: "#21c7ff",
|
|
||||||
alpha: 0.3,
|
|
||||||
thickness: 2,
|
|
||||||
showBorder: true,
|
|
||||||
borderColor: "#ffffff"
|
|
||||||
});
|
|
||||||
|
|
||||||
// 显示放置区域
|
|
||||||
kernel.dropZone.showAll();
|
|
||||||
dropZoneVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 监听放置区域点击事件
|
|
||||||
kernel.on('dropzone:click', async (dropzone_data) => {
|
|
||||||
console.log('点击了放置区域:', dropzone_data);
|
|
||||||
|
|
||||||
const { wallName, index, transform } = dropzone_data;
|
|
||||||
const { position, rotation } = transform;
|
|
||||||
|
|
||||||
// 将模型放置到该区域
|
|
||||||
try {
|
|
||||||
const response = await fetch(`http://localhost:3001/api/product-configs/by-sku/${sku}`);
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.code === 200 && result.data) {
|
|
||||||
console.log('SKU配置数据:', result.data);
|
|
||||||
console.log('关联事件:', result.data.events);
|
|
||||||
|
|
||||||
// 使用 for...of 循环以支持 await
|
|
||||||
for (const event of result.data.events) {
|
|
||||||
if (event.event_type === 'change_model') {
|
|
||||||
console.log(event.target_data);
|
|
||||||
|
|
||||||
const { id, name, file_url, model_control_type, category } = event.target_data;
|
|
||||||
console.log('替换百叶模型:', event);
|
|
||||||
console.log('替换百叶模型类型:', category);
|
|
||||||
|
|
||||||
// 生成唯一的模型ID
|
|
||||||
const modelId = id + '_' + Date.now();
|
|
||||||
|
|
||||||
// 先记录模型放置(会自动处理替换逻辑)
|
|
||||||
kernel.dropZone.recordModelPlacement(wallName, index, modelId);
|
|
||||||
|
|
||||||
// 加载并放置模型
|
|
||||||
await kernel.model.add({
|
|
||||||
modelId: modelId,
|
|
||||||
modelUrl: file_url,
|
|
||||||
modelControlType: model_control_type,
|
|
||||||
drag: {
|
|
||||||
enable: true,
|
|
||||||
axis: Math.abs((rotation.y - 0)) < 90 ? 'x' : 'z',
|
|
||||||
step: 0.1,
|
|
||||||
},
|
|
||||||
transform: {
|
|
||||||
position: position,
|
|
||||||
rotation: rotation,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`百叶模型已放置为 ${name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.event_type === 'change_color') {
|
|
||||||
const materialName = event.material_name;
|
|
||||||
const { color, color_map_url, normal_map_url } = event.target_data;
|
|
||||||
console.log('替换百叶模型颜色:', event.target_data);
|
|
||||||
|
|
||||||
kernel.material.apply({
|
|
||||||
target: materialName,
|
|
||||||
modelId: modelId, // 指定模型ID,只修改该模型的材质
|
|
||||||
albedoColor: color,
|
|
||||||
albedoTexture: color_map_url,
|
|
||||||
normalMap: normal_map_url,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`百叶模型颜色已替换为 ${color}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`未查询到数据`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`查询SKU配置或替换模型失败:`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@ -1,954 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>3D Model Showcase SDK - TS</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
overflow: hidden;
|
|
||||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#canvas-container {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#renderDom {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-panel {
|
|
||||||
width: 320px;
|
|
||||||
background: rgba(30, 30, 45, 0.95);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-panel::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-panel::-webkit-scrollbar-track {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-panel::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-title {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-info {
|
|
||||||
background: rgba(76, 175, 80, 0.2);
|
|
||||||
border: 1px solid rgba(76, 175, 80, 0.5);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-info-title {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #4caf50;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-info-item {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-info-label {
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
min-width: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-info-value {
|
|
||||||
color: #fff;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-category {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header {
|
|
||||||
padding: 12px 15px;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
transition: background 0.3s;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header.active {
|
|
||||||
background: rgba(76, 175, 80, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-arrow {
|
|
||||||
transition: transform 0.3s;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-arrow.expanded {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-content {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 0fr;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: grid-template-rows 0.3s ease, padding 0.3s ease;
|
|
||||||
padding: 0 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-content.expanded {
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-content>* {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-item {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-label {
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-group {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-btn:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border-color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-btn.selected {
|
|
||||||
background: rgba(76, 175, 80, 0.6);
|
|
||||||
border-color: #4CAF50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-checkbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-checkbox:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-checkbox input[type="checkbox"] {
|
|
||||||
margin-right: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-checkbox label {
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 进度条样式 */
|
|
||||||
#progress-container {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 80%;
|
|
||||||
max-width: 500px;
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress-bar {
|
|
||||||
width: 0%;
|
|
||||||
height: 10px;
|
|
||||||
background: linear-gradient(90deg, #4CAF50, #45a049);
|
|
||||||
border-radius: 5px;
|
|
||||||
transition: width 0.1s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress-text {
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 5px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<!-- 画布区域 -->
|
|
||||||
<div id="canvas-container">
|
|
||||||
<canvas id="renderDom"></canvas>
|
|
||||||
<div id="progress-container" style="display: none;">
|
|
||||||
<div id="progress-bar"></div>
|
|
||||||
<div id="progress-text">0%</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 生成放置区域按钮 -->
|
|
||||||
<button id="dropzone-btn" style="
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
left: 20px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: #21c7ff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
||||||
z-index: 100;
|
|
||||||
">生成放置区域</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 配置面板 -->
|
|
||||||
<div id="config-panel">
|
|
||||||
<div class="config-title">选装选配</div>
|
|
||||||
|
|
||||||
<!-- 点击信息显示区域 -->
|
|
||||||
<div id="click-info" class="click-info" style="display: none;">
|
|
||||||
<div class="click-info-title">点击信息</div>
|
|
||||||
<div id="click-info-content"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 棚子尺寸 -->
|
|
||||||
<div class="config-category">
|
|
||||||
<div class="category-header" data-category="size">
|
|
||||||
<span class="category-title">棚子尺寸</span>
|
|
||||||
<span class="category-arrow">▼</span>
|
|
||||||
</div>
|
|
||||||
<div class="category-content">
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="option-btn" data-option="size-1">3x3米</button>
|
|
||||||
<button class="option-btn" data-option="size-2">4x4米</button>
|
|
||||||
<button class="option-btn" data-option="size-3">5x5米</button>
|
|
||||||
<button class="option-btn" data-option="size-4">6x6米</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 棚子类型 -->
|
|
||||||
<div class="config-category">
|
|
||||||
<div class="category-header" data-category="type">
|
|
||||||
<span class="category-title">棚子类型</span>
|
|
||||||
<span class="category-arrow">▼</span>
|
|
||||||
</div>
|
|
||||||
<div class="category-content">
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="option-btn" data-option="type-1">平顶</button>
|
|
||||||
<button class="option-btn" data-option="type-2">尖顶</button>
|
|
||||||
<button class="option-btn" data-option="type-3">弧形</button>
|
|
||||||
<button class="option-btn" data-option="type-4">异形</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 百叶 (单选) -->
|
|
||||||
<div class="config-category">
|
|
||||||
<div class="category-header" data-category="louver">
|
|
||||||
<span class="category-title">百叶</span>
|
|
||||||
<span class="category-arrow">▼</span>
|
|
||||||
</div>
|
|
||||||
<div class="category-content">
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="option-btn" data-option="louver-1">整体</button>
|
|
||||||
<button class="option-btn" data-option="louver-2">3m百叶</button>
|
|
||||||
<button class="option-btn" data-option="louver-3">3m下拉帘</button>
|
|
||||||
<button class="option-btn" data-option="louver-4">百叶4</button>
|
|
||||||
<button class="option-btn" data-option="louver-4">卷帘小</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 配色 -->
|
|
||||||
<div class="config-category">
|
|
||||||
<div class="category-header" data-category="color">
|
|
||||||
<span class="category-title">配色</span>
|
|
||||||
<span class="category-arrow">▼</span>
|
|
||||||
</div>
|
|
||||||
<div class="category-content">
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="option-btn" data-option="color-1">222222</button>
|
|
||||||
<button class="option-btn" data-option="color-2">灰色</button>
|
|
||||||
<button class="option-btn" data-option="color-3">黑色</button>
|
|
||||||
<button class="option-btn" data-option="color-4">木色</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="hotspot-btn">生成热点</button>
|
|
||||||
<button id="prevent-btn">生成防止区域</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 模型信息框(用于2D转3D显示) -->
|
|
||||||
<div id="model-info-box" style="display: none;">
|
|
||||||
<div style="
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
padding: 15px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
||||||
font-size: 14px;
|
|
||||||
color: #333;
|
|
||||||
min-width: 200px;
|
|
||||||
">
|
|
||||||
<div style="font-weight: bold; margin-bottom: 8px; color: #4CAF50;">模型信息</div>
|
|
||||||
<div id="info-name" style="margin-bottom: 5px;">名称: -</div>
|
|
||||||
<div id="info-position" style="margin-bottom: 10px; font-size: 12px; color: #666;">坐标: -</div>
|
|
||||||
|
|
||||||
<!-- 颜色按钮 -->
|
|
||||||
<div id="color-buttons" style="display: none; gap: 8px; margin-bottom: 8px;">
|
|
||||||
<button id="color-btn-1" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #FFFFFF;
|
|
||||||
color: #333;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">白色</button>
|
|
||||||
<button id="color-btn-2" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #000000;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">黑色</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 旋转按钮 -->
|
|
||||||
<div id="rotation-buttons" style="display: none; gap: 8px; margin-bottom: 8px;">
|
|
||||||
<button id="rotation-btn-90" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #2196F3;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">旋转90°</button>
|
|
||||||
<button id="rotation-btn-180" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #2196F3;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">旋转180°</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 8px;">
|
|
||||||
<button id="remove-model-btn" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #f44336;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">移除</button>
|
|
||||||
<button id="close-info-btn" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #4CAF50;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">关闭</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="module" src="./index.js"></script>
|
|
||||||
<script type="module">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { kernel } from './src/main.ts';
|
|
||||||
|
|
||||||
// ========== UI 交互逻辑 ==========
|
|
||||||
|
|
||||||
// 折叠面板切换
|
|
||||||
document.querySelectorAll('.category-header').forEach(header => {
|
|
||||||
header.addEventListener('click', function () {
|
|
||||||
const content = this.nextElementSibling;
|
|
||||||
const arrow = this.querySelector('.category-arrow');
|
|
||||||
|
|
||||||
// 切换展开/收起状态
|
|
||||||
content.classList.toggle('expanded');
|
|
||||||
arrow.classList.toggle('expanded');
|
|
||||||
this.classList.toggle('active');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
let sku = ""
|
|
||||||
// 单选按钮逻辑
|
|
||||||
document.querySelectorAll('.option-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', async function () {
|
|
||||||
const optionGroup = this.parentElement;
|
|
||||||
const category = this.closest('.config-category');
|
|
||||||
const categoryName = category.querySelector('.category-header').dataset.category;
|
|
||||||
|
|
||||||
// 同一组内取消其他选中状态
|
|
||||||
optionGroup.querySelectorAll('.option-btn').forEach(b => {
|
|
||||||
b.classList.remove('selected');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 选中当前按钮
|
|
||||||
this.classList.add('selected');
|
|
||||||
|
|
||||||
// 触发自定义事件
|
|
||||||
const event = new CustomEvent('config:change', {
|
|
||||||
detail: {
|
|
||||||
category: categoryName,
|
|
||||||
value: this.dataset.option,
|
|
||||||
text: this.textContent
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
|
|
||||||
console.log('配置变更:', {
|
|
||||||
category: categoryName,
|
|
||||||
value: this.dataset.option,
|
|
||||||
text: this.textContent
|
|
||||||
});
|
|
||||||
|
|
||||||
// 百叶模型替换逻辑
|
|
||||||
// if (categoryName === "louver") {
|
|
||||||
const currentText = this.textContent;
|
|
||||||
const response = await fetch(`http://localhost:3001/api/product-configs/by-sku/${currentText}`);
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.code === 200) {
|
|
||||||
console.log(result.data);
|
|
||||||
sku = currentText;
|
|
||||||
const { enable_placement_zone,wall_divisions } = result.data;
|
|
||||||
// const {position_x, position_y, position_z} = data;
|
|
||||||
if (enable_placement_zone && wall_divisions != undefined) {
|
|
||||||
await initPlacementZoneConfig(placement_zone);
|
|
||||||
console.log(wall_divisions);
|
|
||||||
|
|
||||||
await placementWall(wall_divisions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// skuToFunc(currentText);
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
document.querySelector('#hotspot-btn').addEventListener('click', async function () {
|
|
||||||
|
|
||||||
await hotspotRequest();
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const hotspotRequest = async () => {
|
|
||||||
try {
|
|
||||||
// 从后端获取激活状态的热点列表
|
|
||||||
const response = await fetch('http://localhost:3001/api/hotspots?status=active&page=1&pageSize=100');
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.code === 200 && result.data.list.length > 0) {
|
|
||||||
// 将后端数据转换为 SDK 需要的格式
|
|
||||||
const hotspots = result.data.list.map(item => ({
|
|
||||||
id: item.id,
|
|
||||||
type: 'hotspot',
|
|
||||||
name: item.name,
|
|
||||||
meshName: item.name, // 可以根据实际情况调整
|
|
||||||
icon: item.image_url,
|
|
||||||
position: [item.position_x, item.position_y, item.position_z],
|
|
||||||
radius: item.radius,
|
|
||||||
color: "#000000",
|
|
||||||
payload: {
|
|
||||||
skus: item.skus || [],
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 渲染热点
|
|
||||||
kernel.hotspot.render(hotspots);
|
|
||||||
console.log('热点渲染成功:', hotspots);
|
|
||||||
} else {
|
|
||||||
console.log('没有可用的热点数据');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取热点数据失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听热点点击事件
|
|
||||||
window.addEventListener('hotspot:click', (event) => {
|
|
||||||
console.log('热点被点击:', event.detail);
|
|
||||||
const { id, name, payload } = event.detail;
|
|
||||||
|
|
||||||
const clickInfoDiv = document.getElementById('click-info');
|
|
||||||
const clickInfoContent = document.getElementById('click-info-content');
|
|
||||||
|
|
||||||
let html = `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">类型:</span>
|
|
||||||
<span class="click-info-value">热点</span>
|
|
||||||
</div>
|
|
||||||
<div class="click-info-item">
|
|
||||||
<span class="click-info-label">名称:</span>
|
|
||||||
<span class="click-info-value">${name}</span>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
if (payload && payload.skus && payload.skus.length > 0) {
|
|
||||||
html += `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">关联SKU:</span>
|
|
||||||
<span class="click-info-value">${payload.skus.join(', ')}</span>
|
|
||||||
</div>`;
|
|
||||||
} else {
|
|
||||||
html += `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">关联SKU:</span>
|
|
||||||
<span class="click-info-value">无</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
clickInfoContent.innerHTML = html;
|
|
||||||
clickInfoDiv.style.display = 'block';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听模型点击事件
|
|
||||||
window.addEventListener('model:click', (event) => {
|
|
||||||
console.log('模型被点击:', event.detail);
|
|
||||||
const { meshName, materialName, modelControlType } = event.detail;
|
|
||||||
|
|
||||||
const clickInfoDiv = document.getElementById('click-info');
|
|
||||||
const clickInfoContent = document.getElementById('click-info-content');
|
|
||||||
|
|
||||||
let html = `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">类型:</span>
|
|
||||||
<span class="click-info-value">模型</span>
|
|
||||||
</div>
|
|
||||||
<div class="click-info-item">
|
|
||||||
<span class="click-info-label">网格名称:</span>
|
|
||||||
<span class="click-info-value">${meshName}</span>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
if (materialName) {
|
|
||||||
html += `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">材质名称:</span>
|
|
||||||
<span class="click-info-value">${materialName}</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modelControlType) {
|
|
||||||
html += `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">控制类型:</span>
|
|
||||||
<span class="click-info-value">${modelControlType}</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
clickInfoContent.innerHTML = html;
|
|
||||||
clickInfoDiv.style.display = 'block';
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// 多选复选框逻辑
|
|
||||||
document.querySelectorAll('.option-checkbox input[type="checkbox"]').forEach(checkbox => {
|
|
||||||
checkbox.addEventListener('change', async function () {
|
|
||||||
const category = this.closest('.config-category');
|
|
||||||
const categoryName = category.querySelector('.category-header').dataset.category;
|
|
||||||
const optionGroup = this.closest('.option-group');
|
|
||||||
const checked = this.checked;
|
|
||||||
|
|
||||||
// 获取当前组所有选中的值
|
|
||||||
const selectedValues = Array.from(
|
|
||||||
optionGroup.querySelectorAll('input[type="checkbox"]:checked')
|
|
||||||
).map(cb => ({
|
|
||||||
value: cb.dataset.option,
|
|
||||||
text: cb.nextElementSibling.textContent
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 触发自定义事件
|
|
||||||
const event = new CustomEvent('config:change', {
|
|
||||||
detail: {
|
|
||||||
category: categoryName,
|
|
||||||
values: selectedValues,
|
|
||||||
checked: this.checked,
|
|
||||||
currentValue: this.dataset.option
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
|
|
||||||
console.log('配置变更(多选):', {
|
|
||||||
category: categoryName,
|
|
||||||
selectedValues: selectedValues,
|
|
||||||
checked: this.checked,
|
|
||||||
currentValue: this.dataset.option
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听配置变更事件(供外部使用)
|
|
||||||
document.addEventListener('config:change', function (e) {
|
|
||||||
// 这里可以根据配置变更来操作 3D 模型
|
|
||||||
// 例如:
|
|
||||||
// if (e.detail.category === 'size') {
|
|
||||||
// kernel.model.replace({ modelId: 'shed', modelUrl: `/models/shed-${e.detail.value}.glb`, modelControlType: 'rotation' });
|
|
||||||
// }
|
|
||||||
// if (e.detail.category === 'color') {
|
|
||||||
// kernel.material.apply({
|
|
||||||
// target: 'ShedMaterial',
|
|
||||||
// attribute: 'baseColor',
|
|
||||||
// value: getColorValue(e.detail.value)
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========== 模型信息框按钮事件 ==========
|
|
||||||
|
|
||||||
// 关闭按钮事件
|
|
||||||
document.getElementById('close-info-btn').addEventListener('click', () => {
|
|
||||||
kernel.domTo3D.detach('model-info');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 白色按钮事件
|
|
||||||
document.getElementById('color-btn-1').addEventListener('click', () => {
|
|
||||||
const materialName = window.getCurrentMaterialName();
|
|
||||||
if (materialName) {
|
|
||||||
console.log('切换为白色,材质名:', materialName);
|
|
||||||
kernel.material.apply({
|
|
||||||
target: materialName,
|
|
||||||
albedoColor: '#FFFFFF',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('没有选中材质');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 黑色按钮事件
|
|
||||||
document.getElementById('color-btn-2').addEventListener('click', () => {
|
|
||||||
const materialName = window.getCurrentMaterialName();
|
|
||||||
if (materialName) {
|
|
||||||
console.log('切换为黑色,材质名:', materialName);
|
|
||||||
kernel.material.apply({
|
|
||||||
target: materialName,
|
|
||||||
albedoColor: '#000000',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('没有选中材质');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 旋转90度按钮事件
|
|
||||||
document.getElementById('rotation-btn-90').addEventListener('click', () => {
|
|
||||||
const pickedMesh = window.getCurrentPickedMesh();
|
|
||||||
if (pickedMesh) {
|
|
||||||
const modelName = kernel.model.findModelNameByMesh?.(pickedMesh);
|
|
||||||
if (modelName) {
|
|
||||||
console.log('旋转90度,模型名:', modelName);
|
|
||||||
kernel.transform.rotation({
|
|
||||||
modelId: modelName,
|
|
||||||
vector3: { x: 0, y: 90, z: 0 }
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('未找到模型名称');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('没有选中的网格');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 旋转180度按钮事件
|
|
||||||
document.getElementById('rotation-btn-180').addEventListener('click', () => {
|
|
||||||
const pickedMesh = window.getCurrentPickedMesh();
|
|
||||||
if (pickedMesh) {
|
|
||||||
const modelName = kernel.model.findModelNameByMesh?.(pickedMesh);
|
|
||||||
if (modelName) {
|
|
||||||
console.log('旋转180度,模型名:', modelName);
|
|
||||||
kernel.transform.rotation({
|
|
||||||
modelId: modelName,
|
|
||||||
vector3: { x: 0, y: 30, z: 0 }
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('未找到模型名称');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('没有选中的网格');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 移除按钮事件
|
|
||||||
document.getElementById('remove-model-btn').addEventListener('click', () => {
|
|
||||||
const pickedMesh = window.getCurrentPickedMesh();
|
|
||||||
if (pickedMesh) {
|
|
||||||
const meshName = pickedMesh.name;
|
|
||||||
const success = kernel.model.remove(meshName);
|
|
||||||
if (success) {
|
|
||||||
console.log('模型已移除');
|
|
||||||
// 关闭信息框
|
|
||||||
kernel.domTo3D.detach('model-info');
|
|
||||||
} else {
|
|
||||||
console.log('移除失败:未找到该网格所属的模型');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('没有选中的网格');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 生成放置区域按钮事件
|
|
||||||
let dropZoneVisible = false;
|
|
||||||
document.getElementById('dropzone-btn').addEventListener('click', () => {
|
|
||||||
if (!dropZoneVisible) {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 更新按钮文字
|
|
||||||
document.getElementById('dropzone-btn').textContent = '隐藏放置区域';
|
|
||||||
console.log('已生成并显示放置区域');
|
|
||||||
} else {
|
|
||||||
// 隐藏放置区域
|
|
||||||
kernel.dropZone.hideAll();
|
|
||||||
dropZoneVisible = false;
|
|
||||||
|
|
||||||
// 更新按钮文字
|
|
||||||
document.getElementById('dropzone-btn').textContent = '生成放置区域';
|
|
||||||
console.log('已隐藏放置区域');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化放置区域配置数据(只需设置一次)
|
|
||||||
const initPlacementZoneConfig = (placement_zone) => {
|
|
||||||
|
|
||||||
// 只清除旧的放置区域网格,不清除模型
|
|
||||||
kernel.dropZone.clearZones();
|
|
||||||
// 调整 baseY 来控制整体高度(正数向上,负数向下)
|
|
||||||
const baseY = 0.09; // 修改这个值来调整整体高度
|
|
||||||
const height = 2.27;
|
|
||||||
// 调整 offset 来控制每个面向外或向内的偏移
|
|
||||||
// 正数 = 向外移动,负数 = 向内移动
|
|
||||||
const wallOffset = -0.07; // 修改这个值来调整墙面偏移
|
|
||||||
|
|
||||||
kernel.dropZone.setData({
|
|
||||||
|
|
||||||
color: "#21c7ff",
|
|
||||||
alpha: 0.3,
|
|
||||||
thickness: 2,
|
|
||||||
showBorder: true,
|
|
||||||
borderColor: "#ffffff",
|
|
||||||
walls: [
|
|
||||||
{
|
|
||||||
name: 'front',
|
|
||||||
startPoint: [-1.82, baseY, -1.37],
|
|
||||||
endPoint: [1.87, baseY, -1.37],
|
|
||||||
height: height,
|
|
||||||
divisions: divisions,
|
|
||||||
offset: wallOffset // 向外或向内偏移
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'back',
|
|
||||||
startPoint: [1.87, baseY, 1.4],
|
|
||||||
endPoint: [-1.82, baseY, 1.4],
|
|
||||||
height: height,
|
|
||||||
divisions: divisions,
|
|
||||||
offset: wallOffset
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'left',
|
|
||||||
startPoint: [-1.82, baseY, 1.4],
|
|
||||||
endPoint: [-1.82, baseY, -1.37],
|
|
||||||
height: height,
|
|
||||||
divisions: divisions,
|
|
||||||
offset: wallOffset
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'right',
|
|
||||||
startPoint: [1.82, baseY, -1.37],
|
|
||||||
endPoint: [1.82, baseY, 1.4],
|
|
||||||
height: height,
|
|
||||||
divisions: divisions,
|
|
||||||
offset: wallOffset
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
kernel.dropZone.generateDropZones(divisions);
|
|
||||||
// 显示放置区域
|
|
||||||
kernel.dropZone.show();
|
|
||||||
dropZoneVisible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const placementWall = (walls) => {
|
|
||||||
// 只清除旧的放置区域网格,不清除模型
|
|
||||||
kernel.dropZone.clearZones();
|
|
||||||
const divisions = walls.map(wall => ({
|
|
||||||
name: wall.name, // 获取最后一个下划线后的部分
|
|
||||||
divisions: wall.divisions
|
|
||||||
}))
|
|
||||||
console.log(divisions);
|
|
||||||
|
|
||||||
kernel.dropZone.updateDivisions(divisions);
|
|
||||||
// 显示放置区域
|
|
||||||
kernel.dropZone.show();
|
|
||||||
dropZoneVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 监听放置区域点击事件
|
|
||||||
kernel.on('dropzone:click', async (dropzone_data) => {
|
|
||||||
console.log('点击了放置区域:', dropzone_data);
|
|
||||||
|
|
||||||
const { wallName, index, transform } = dropzone_data;
|
|
||||||
const { position, rotation } = transform;
|
|
||||||
|
|
||||||
// 将模型放置到该区域
|
|
||||||
try {
|
|
||||||
const response = await fetch(`http://localhost:3001/api/product-configs/by-sku/${sku}`);
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.code === 200 && result.data) {
|
|
||||||
console.log('SKU配置数据:', result.data);
|
|
||||||
console.log('关联事件:', result.data.events);
|
|
||||||
|
|
||||||
// 使用 for...of 循环以支持 await
|
|
||||||
for (const event of result.data.events) {
|
|
||||||
if (event.event_type === 'change_model') {
|
|
||||||
console.log(event.target_data);
|
|
||||||
|
|
||||||
const { id, name, file_url, model_control_type, category } = event.target_data;
|
|
||||||
console.log('替换百叶模型:', event);
|
|
||||||
console.log('替换百叶模型类型:', category);
|
|
||||||
|
|
||||||
// 生成唯一的模型ID
|
|
||||||
const modelId = id + '_' + Date.now();
|
|
||||||
|
|
||||||
// 先记录模型放置(会自动处理替换逻辑)
|
|
||||||
kernel.dropZone.recordModelPlacement(wallName, index, modelId);
|
|
||||||
|
|
||||||
// 加载并放置模型
|
|
||||||
await kernel.model.add({
|
|
||||||
modelId: modelId,
|
|
||||||
modelUrl: file_url,
|
|
||||||
modelControlType: model_control_type,
|
|
||||||
drag: {
|
|
||||||
enable: true,
|
|
||||||
axis: Math.abs((rotation.y - 0)) < 90 ? 'x' : 'z',
|
|
||||||
step: 0.1,
|
|
||||||
},
|
|
||||||
transform: {
|
|
||||||
position: position,
|
|
||||||
rotation: rotation,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`百叶模型已放置为 ${name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.event_type === 'change_color') {
|
|
||||||
const materialName = event.material_name;
|
|
||||||
const { color, color_map_url, normal_map_url } = event.target_data;
|
|
||||||
console.log('替换百叶模型颜色:', event.target_data);
|
|
||||||
|
|
||||||
kernel.material.apply({
|
|
||||||
target: materialName,
|
|
||||||
modelId: modelId, // 指定模型ID,只修改该模型的材质
|
|
||||||
albedoColor: color,
|
|
||||||
albedoTexture: color_map_url,
|
|
||||||
normalMap: normal_map_url,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`百叶模型颜色已替换为 ${color}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`未查询到数据`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`查询SKU配置或替换模型失败:`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
957
index copy.html
957
index copy.html
@ -1,957 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>3D Model Showcase SDK - TS</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
overflow: hidden;
|
|
||||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#canvas-container {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#renderDom {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-panel {
|
|
||||||
width: 320px;
|
|
||||||
background: rgba(30, 30, 45, 0.95);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-panel::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-panel::-webkit-scrollbar-track {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#config-panel::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-title {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-info {
|
|
||||||
background: rgba(76, 175, 80, 0.2);
|
|
||||||
border: 1px solid rgba(76, 175, 80, 0.5);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-info-title {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #4caf50;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-info-item {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-info-label {
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
min-width: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-info-value {
|
|
||||||
color: #fff;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-category {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header {
|
|
||||||
padding: 12px 15px;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
transition: background 0.3s;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header.active {
|
|
||||||
background: rgba(76, 175, 80, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-arrow {
|
|
||||||
transition: transform 0.3s;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-arrow.expanded {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-content {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: 0fr;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: grid-template-rows 0.3s ease, padding 0.3s ease;
|
|
||||||
padding: 0 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-content.expanded {
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-content>* {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-item {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-label {
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-group {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-btn {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-btn:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border-color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-btn.selected {
|
|
||||||
background: rgba(76, 175, 80, 0.6);
|
|
||||||
border-color: #4CAF50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-checkbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-checkbox:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-checkbox input[type="checkbox"] {
|
|
||||||
margin-right: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-checkbox label {
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 进度条样式 */
|
|
||||||
#progress-container {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 80%;
|
|
||||||
max-width: 500px;
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress-bar {
|
|
||||||
width: 0%;
|
|
||||||
height: 10px;
|
|
||||||
background: linear-gradient(90deg, #4CAF50, #45a049);
|
|
||||||
border-radius: 5px;
|
|
||||||
transition: width 0.1s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
#progress-text {
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 5px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<!-- 画布区域 -->
|
|
||||||
<div id="canvas-container">
|
|
||||||
<canvas id="renderDom"></canvas>
|
|
||||||
<div id="progress-container" style="display: none;">
|
|
||||||
<div id="progress-bar"></div>
|
|
||||||
<div id="progress-text">0%</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 生成放置区域按钮 -->
|
|
||||||
<button id="dropzone-btn" style="
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
left: 20px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: #21c7ff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
||||||
z-index: 100;
|
|
||||||
">生成放置区域</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 配置面板 -->
|
|
||||||
<div id="config-panel">
|
|
||||||
<div class="config-title">选装选配</div>
|
|
||||||
|
|
||||||
<!-- 点击信息显示区域 -->
|
|
||||||
<div id="click-info" class="click-info" style="display: none;">
|
|
||||||
<div class="click-info-title">点击信息</div>
|
|
||||||
<div id="click-info-content"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 棚子尺寸 -->
|
|
||||||
<div class="config-category">
|
|
||||||
<div class="category-header" data-category="size">
|
|
||||||
<span class="category-title">棚子尺寸</span>
|
|
||||||
<span class="category-arrow">▼</span>
|
|
||||||
</div>
|
|
||||||
<div class="category-content">
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="option-btn" data-option="size-1">3x3米</button>
|
|
||||||
<button class="option-btn" data-option="size-2">4x4米</button>
|
|
||||||
<button class="option-btn" data-option="size-3">5x5米</button>
|
|
||||||
<button class="option-btn" data-option="size-4">6x6米</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 棚子类型 -->
|
|
||||||
<div class="config-category">
|
|
||||||
<div class="category-header" data-category="type">
|
|
||||||
<span class="category-title">棚子类型</span>
|
|
||||||
<span class="category-arrow">▼</span>
|
|
||||||
</div>
|
|
||||||
<div class="category-content">
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="option-btn" data-option="type-1">平顶</button>
|
|
||||||
<button class="option-btn" data-option="type-2">尖顶</button>
|
|
||||||
<button class="option-btn" data-option="type-3">弧形</button>
|
|
||||||
<button class="option-btn" data-option="type-4">异形</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 百叶 (单选) -->
|
|
||||||
<div class="config-category">
|
|
||||||
<div class="category-header" data-category="louver">
|
|
||||||
<span class="category-title">百叶</span>
|
|
||||||
<span class="category-arrow">▼</span>
|
|
||||||
</div>
|
|
||||||
<div class="category-content">
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="option-btn" data-option="louver-1">111</button>
|
|
||||||
<button class="option-btn" data-option="louver-2">百叶2</button>
|
|
||||||
<button class="option-btn" data-option="louver-3">百叶3</button>
|
|
||||||
<button class="option-btn" data-option="louver-4">百叶4</button>
|
|
||||||
<button class="option-btn" data-option="louver-4">卷帘小</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 配色 -->
|
|
||||||
<div class="config-category">
|
|
||||||
<div class="category-header" data-category="color">
|
|
||||||
<span class="category-title">配色</span>
|
|
||||||
<span class="category-arrow">▼</span>
|
|
||||||
</div>
|
|
||||||
<div class="category-content">
|
|
||||||
<div class="option-group">
|
|
||||||
<button class="option-btn" data-option="color-1">222222</button>
|
|
||||||
<button class="option-btn" data-option="color-2">灰色</button>
|
|
||||||
<button class="option-btn" data-option="color-3">黑色</button>
|
|
||||||
<button class="option-btn" data-option="color-4">木色</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="hotspot-btn">生成热点</button>
|
|
||||||
<button id="prevent-btn">生成防止区域</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 模型信息框(用于2D转3D显示) -->
|
|
||||||
<div id="model-info-box" style="display: none;">
|
|
||||||
<div style="
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
padding: 15px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
||||||
font-size: 14px;
|
|
||||||
color: #333;
|
|
||||||
min-width: 200px;
|
|
||||||
">
|
|
||||||
<div style="font-weight: bold; margin-bottom: 8px; color: #4CAF50;">模型信息</div>
|
|
||||||
<div id="info-name" style="margin-bottom: 5px;">名称: -</div>
|
|
||||||
<div id="info-position" style="margin-bottom: 10px; font-size: 12px; color: #666;">坐标: -</div>
|
|
||||||
|
|
||||||
<!-- 颜色按钮 -->
|
|
||||||
<div id="color-buttons" style="display: none; gap: 8px; margin-bottom: 8px;">
|
|
||||||
<button id="color-btn-1" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #FFFFFF;
|
|
||||||
color: #333;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">白色</button>
|
|
||||||
<button id="color-btn-2" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #000000;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">黑色</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 旋转按钮 -->
|
|
||||||
<div id="rotation-buttons" style="display: none; gap: 8px; margin-bottom: 8px;">
|
|
||||||
<button id="rotation-btn-90" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #2196F3;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">旋转90°</button>
|
|
||||||
<button id="rotation-btn-180" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #2196F3;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">旋转180°</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 8px;">
|
|
||||||
<button id="remove-model-btn" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #f44336;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">移除</button>
|
|
||||||
<button id="close-info-btn" style="
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
background: #4CAF50;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
">关闭</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="module" src="./index.js"></script>
|
|
||||||
<script type="module">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import { kernel } from './src/main.ts';
|
|
||||||
|
|
||||||
// ========== UI 交互逻辑 ==========
|
|
||||||
|
|
||||||
// 折叠面板切换
|
|
||||||
document.querySelectorAll('.category-header').forEach(header => {
|
|
||||||
header.addEventListener('click', function () {
|
|
||||||
const content = this.nextElementSibling;
|
|
||||||
const arrow = this.querySelector('.category-arrow');
|
|
||||||
|
|
||||||
// 切换展开/收起状态
|
|
||||||
content.classList.toggle('expanded');
|
|
||||||
arrow.classList.toggle('expanded');
|
|
||||||
this.classList.toggle('active');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 单选按钮逻辑
|
|
||||||
document.querySelectorAll('.option-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', async function () {
|
|
||||||
const optionGroup = this.parentElement;
|
|
||||||
const category = this.closest('.config-category');
|
|
||||||
const categoryName = category.querySelector('.category-header').dataset.category;
|
|
||||||
|
|
||||||
// 同一组内取消其他选中状态
|
|
||||||
optionGroup.querySelectorAll('.option-btn').forEach(b => {
|
|
||||||
b.classList.remove('selected');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 选中当前按钮
|
|
||||||
this.classList.add('selected');
|
|
||||||
|
|
||||||
// 触发自定义事件
|
|
||||||
const event = new CustomEvent('config:change', {
|
|
||||||
detail: {
|
|
||||||
category: categoryName,
|
|
||||||
value: this.dataset.option,
|
|
||||||
text: this.textContent
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
|
|
||||||
console.log('配置变更:', {
|
|
||||||
category: categoryName,
|
|
||||||
value: this.dataset.option,
|
|
||||||
text: this.textContent
|
|
||||||
});
|
|
||||||
|
|
||||||
// 百叶模型替换逻辑
|
|
||||||
// if (categoryName === "louver") {
|
|
||||||
const currentText = this.textContent;
|
|
||||||
console.log(currentText);
|
|
||||||
|
|
||||||
skuToFunc(currentText);
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
document.querySelector('#hotspot-btn').addEventListener('click', async function () {
|
|
||||||
|
|
||||||
await hotspotRequest();
|
|
||||||
})
|
|
||||||
const skuToFunc = async (currentText) => {
|
|
||||||
// 根据 SKU 查询配置和事件
|
|
||||||
try {
|
|
||||||
const response = await fetch(`http://localhost:3000/api/product-configs/by-sku/${currentText}`);
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.code === 200 && result.data) {
|
|
||||||
console.log('SKU配置数据:', result.data);
|
|
||||||
console.log('关联事件:', result.data.events);
|
|
||||||
placementWall(1);
|
|
||||||
// 使用 for...of 循环以支持 await
|
|
||||||
for (const event of result.data.events) {
|
|
||||||
if (event.event_type === 'change_model') {
|
|
||||||
const { file_url, model_control_type, category } = event.target_data;
|
|
||||||
console.log('替换百叶模型:', event);
|
|
||||||
|
|
||||||
await kernel.model.replace({
|
|
||||||
modelId: category,
|
|
||||||
modelUrl: file_url,
|
|
||||||
modelControlType: model_control_type,
|
|
||||||
drag: {
|
|
||||||
enable: true,
|
|
||||||
axis: 'x',
|
|
||||||
step: 0.1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`百叶模型已替换为 ${currentText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.event_type === 'change_color') {
|
|
||||||
const materialName = event.material_name;
|
|
||||||
const { color, color_map_url, normal_map_url } = event.target_data;
|
|
||||||
console.log('替换百叶模型颜色:', event.target_data);
|
|
||||||
|
|
||||||
kernel.material.apply({
|
|
||||||
target: materialName,
|
|
||||||
albedoColor: color,
|
|
||||||
albedoTexture: color_map_url,
|
|
||||||
normalMap: normal_map_url,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`百叶模型颜色已替换为 ${color}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`未查询到数据`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`查询SKU配置或替换模型失败:`, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const hotspotRequest = async () => {
|
|
||||||
try {
|
|
||||||
// 从后端获取激活状态的热点列表
|
|
||||||
const response = await fetch('http://localhost:3001/api/hotspots?status=active&page=1&pageSize=100');
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.code === 200 && result.data.list.length > 0) {
|
|
||||||
// 将后端数据转换为 SDK 需要的格式
|
|
||||||
const hotspots = result.data.list.map(item => ({
|
|
||||||
id: item.id,
|
|
||||||
type: 'hotspot',
|
|
||||||
name: item.name,
|
|
||||||
meshName: item.name, // 可以根据实际情况调整
|
|
||||||
icon: item.image_url,
|
|
||||||
position: [item.position_x, item.position_y, item.position_z],
|
|
||||||
radius: item.radius,
|
|
||||||
color: "#000000",
|
|
||||||
payload: {
|
|
||||||
skus: item.skus || [],
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 渲染热点
|
|
||||||
kernel.hotspot.render(hotspots);
|
|
||||||
console.log('热点渲染成功:', hotspots);
|
|
||||||
} else {
|
|
||||||
console.log('没有可用的热点数据');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取热点数据失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听热点点击事件
|
|
||||||
window.addEventListener('hotspot:click', (event) => {
|
|
||||||
console.log('热点被点击:', event.detail);
|
|
||||||
const { id, name, payload } = event.detail;
|
|
||||||
|
|
||||||
const clickInfoDiv = document.getElementById('click-info');
|
|
||||||
const clickInfoContent = document.getElementById('click-info-content');
|
|
||||||
|
|
||||||
let html = `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">类型:</span>
|
|
||||||
<span class="click-info-value">热点</span>
|
|
||||||
</div>
|
|
||||||
<div class="click-info-item">
|
|
||||||
<span class="click-info-label">名称:</span>
|
|
||||||
<span class="click-info-value">${name}</span>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
if (payload && payload.skus && payload.skus.length > 0) {
|
|
||||||
html += `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">关联SKU:</span>
|
|
||||||
<span class="click-info-value">${payload.skus.join(', ')}</span>
|
|
||||||
</div>`;
|
|
||||||
} else {
|
|
||||||
html += `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">关联SKU:</span>
|
|
||||||
<span class="click-info-value">无</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
clickInfoContent.innerHTML = html;
|
|
||||||
clickInfoDiv.style.display = 'block';
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听模型点击事件
|
|
||||||
window.addEventListener('model:click', (event) => {
|
|
||||||
console.log('模型被点击:', event.detail);
|
|
||||||
const { meshName, materialName, modelControlType } = event.detail;
|
|
||||||
|
|
||||||
const clickInfoDiv = document.getElementById('click-info');
|
|
||||||
const clickInfoContent = document.getElementById('click-info-content');
|
|
||||||
|
|
||||||
let html = `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">类型:</span>
|
|
||||||
<span class="click-info-value">模型</span>
|
|
||||||
</div>
|
|
||||||
<div class="click-info-item">
|
|
||||||
<span class="click-info-label">网格名称:</span>
|
|
||||||
<span class="click-info-value">${meshName}</span>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
if (materialName) {
|
|
||||||
html += `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">材质名称:</span>
|
|
||||||
<span class="click-info-value">${materialName}</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modelControlType) {
|
|
||||||
html += `<div class="click-info-item">
|
|
||||||
<span class="click-info-label">控制类型:</span>
|
|
||||||
<span class="click-info-value">${modelControlType}</span>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
clickInfoContent.innerHTML = html;
|
|
||||||
clickInfoDiv.style.display = 'block';
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// 多选复选框逻辑
|
|
||||||
document.querySelectorAll('.option-checkbox input[type="checkbox"]').forEach(checkbox => {
|
|
||||||
checkbox.addEventListener('change', async function () {
|
|
||||||
const category = this.closest('.config-category');
|
|
||||||
const categoryName = category.querySelector('.category-header').dataset.category;
|
|
||||||
const optionGroup = this.closest('.option-group');
|
|
||||||
const checked = this.checked;
|
|
||||||
|
|
||||||
// 获取当前组所有选中的值
|
|
||||||
const selectedValues = Array.from(
|
|
||||||
optionGroup.querySelectorAll('input[type="checkbox"]:checked')
|
|
||||||
).map(cb => ({
|
|
||||||
value: cb.dataset.option,
|
|
||||||
text: cb.nextElementSibling.textContent
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 触发自定义事件
|
|
||||||
const event = new CustomEvent('config:change', {
|
|
||||||
detail: {
|
|
||||||
category: categoryName,
|
|
||||||
values: selectedValues,
|
|
||||||
checked: this.checked,
|
|
||||||
currentValue: this.dataset.option
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
|
|
||||||
console.log('配置变更(多选):', {
|
|
||||||
category: categoryName,
|
|
||||||
selectedValues: selectedValues,
|
|
||||||
checked: this.checked,
|
|
||||||
currentValue: this.dataset.option
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听配置变更事件(供外部使用)
|
|
||||||
document.addEventListener('config:change', function (e) {
|
|
||||||
// 这里可以根据配置变更来操作 3D 模型
|
|
||||||
// 例如:
|
|
||||||
// if (e.detail.category === 'size') {
|
|
||||||
// kernel.model.replace({ modelId: 'shed', modelUrl: `/models/shed-${e.detail.value}.glb`, modelControlType: 'rotation' });
|
|
||||||
// }
|
|
||||||
// if (e.detail.category === 'color') {
|
|
||||||
// kernel.material.apply({
|
|
||||||
// target: 'ShedMaterial',
|
|
||||||
// attribute: 'baseColor',
|
|
||||||
// value: getColorValue(e.detail.value)
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========== 模型信息框按钮事件 ==========
|
|
||||||
|
|
||||||
// 关闭按钮事件
|
|
||||||
document.getElementById('close-info-btn').addEventListener('click', () => {
|
|
||||||
kernel.domTo3D.detach('model-info');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 白色按钮事件
|
|
||||||
document.getElementById('color-btn-1').addEventListener('click', () => {
|
|
||||||
const materialName = window.getCurrentMaterialName();
|
|
||||||
if (materialName) {
|
|
||||||
console.log('切换为白色,材质名:', materialName);
|
|
||||||
kernel.material.apply({
|
|
||||||
target: materialName,
|
|
||||||
albedoColor: '#FFFFFF',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('没有选中材质');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 黑色按钮事件
|
|
||||||
document.getElementById('color-btn-2').addEventListener('click', () => {
|
|
||||||
const materialName = window.getCurrentMaterialName();
|
|
||||||
if (materialName) {
|
|
||||||
console.log('切换为黑色,材质名:', materialName);
|
|
||||||
kernel.material.apply({
|
|
||||||
target: materialName,
|
|
||||||
albedoColor: '#000000',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('没有选中材质');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 旋转90度按钮事件
|
|
||||||
document.getElementById('rotation-btn-90').addEventListener('click', () => {
|
|
||||||
const pickedMesh = window.getCurrentPickedMesh();
|
|
||||||
if (pickedMesh) {
|
|
||||||
const modelName = kernel.model.findModelNameByMesh?.(pickedMesh);
|
|
||||||
if (modelName) {
|
|
||||||
console.log('旋转90度,模型名:', modelName);
|
|
||||||
kernel.transform.rotation({
|
|
||||||
modelId: modelName,
|
|
||||||
vector3: { x: 0, y: 90, z: 0 }
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('未找到模型名称');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('没有选中的网格');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 旋转180度按钮事件
|
|
||||||
document.getElementById('rotation-btn-180').addEventListener('click', () => {
|
|
||||||
const pickedMesh = window.getCurrentPickedMesh();
|
|
||||||
if (pickedMesh) {
|
|
||||||
const modelName = kernel.model.findModelNameByMesh?.(pickedMesh);
|
|
||||||
if (modelName) {
|
|
||||||
console.log('旋转180度,模型名:', modelName);
|
|
||||||
kernel.transform.rotation({
|
|
||||||
modelId: modelName,
|
|
||||||
vector3: { x: 0, y: 30, z: 0 }
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('未找到模型名称');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('没有选中的网格');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 移除按钮事件
|
|
||||||
document.getElementById('remove-model-btn').addEventListener('click', () => {
|
|
||||||
const pickedMesh = window.getCurrentPickedMesh();
|
|
||||||
if (pickedMesh) {
|
|
||||||
const meshName = pickedMesh.name;
|
|
||||||
const success = kernel.model.remove(meshName);
|
|
||||||
if (success) {
|
|
||||||
console.log('模型已移除');
|
|
||||||
// 关闭信息框
|
|
||||||
kernel.domTo3D.detach('model-info');
|
|
||||||
} else {
|
|
||||||
console.log('移除失败:未找到该网格所属的模型');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('没有选中的网格');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 生成放置区域按钮事件
|
|
||||||
let dropZoneVisible = false;
|
|
||||||
document.getElementById('dropzone-btn').addEventListener('click', () => {
|
|
||||||
if (!dropZoneVisible) {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 更新按钮文字
|
|
||||||
document.getElementById('dropzone-btn').textContent = '隐藏放置区域';
|
|
||||||
console.log('已生成并显示放置区域');
|
|
||||||
} else {
|
|
||||||
// 隐藏放置区域
|
|
||||||
kernel.dropZone.hideAll();
|
|
||||||
dropZoneVisible = false;
|
|
||||||
|
|
||||||
// 更新按钮文字
|
|
||||||
document.getElementById('dropzone-btn').textContent = '生成放置区域';
|
|
||||||
console.log('已隐藏放置区域');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const placementWall = (divisions) => {
|
|
||||||
|
|
||||||
// 先清除旧的放置区域
|
|
||||||
kernel.dropZone.clearAll();
|
|
||||||
|
|
||||||
// 生成新的放置区域(使用新的墙面参数化API)
|
|
||||||
// 调整 baseY 来控制整体高度(正数向上,负数向下)
|
|
||||||
const baseY = 0.08; // 修改这个值来调整整体高度
|
|
||||||
|
|
||||||
// 调整 offset 来控制每个面向外或向内的偏移
|
|
||||||
// 正数 = 向外移动,负数 = 向内移动
|
|
||||||
const wallOffset = -0.07; // 修改这个值来调整墙面偏移
|
|
||||||
|
|
||||||
kernel.dropZone.generate({
|
|
||||||
walls: [
|
|
||||||
{
|
|
||||||
name: 'front',
|
|
||||||
startPoint: [-1.43, baseY, -1.4],
|
|
||||||
endPoint: [1.37, baseY, -1.4],
|
|
||||||
height: 2.2,
|
|
||||||
divisions: divisions,
|
|
||||||
offset: wallOffset // 向外或向内偏移
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'back',
|
|
||||||
startPoint: [1.37, baseY, 1.4],
|
|
||||||
endPoint: [-1.43, baseY, 1.4],
|
|
||||||
height: 2.2,
|
|
||||||
divisions: divisions,
|
|
||||||
offset: wallOffset
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'left',
|
|
||||||
startPoint: [-1.43, baseY, 1.39],
|
|
||||||
endPoint: [-1.43, baseY, -1.43],
|
|
||||||
height: 2.2,
|
|
||||||
divisions: divisions,
|
|
||||||
offset: wallOffset
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'right',
|
|
||||||
startPoint: [1.37, baseY, -1.43],
|
|
||||||
endPoint: [1.37, baseY, 1.4],
|
|
||||||
height: 2.2,
|
|
||||||
divisions: divisions,
|
|
||||||
offset: wallOffset
|
|
||||||
}
|
|
||||||
],
|
|
||||||
color: "#21c7ff",
|
|
||||||
alpha: 0.3,
|
|
||||||
thickness: 2,
|
|
||||||
showBorder: true,
|
|
||||||
borderColor: "#ffffff"
|
|
||||||
});
|
|
||||||
|
|
||||||
// 显示放置区域
|
|
||||||
kernel.dropZone.showAll();
|
|
||||||
dropZoneVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 监听放置区域点击事件
|
|
||||||
kernel.on('dropzone:click', async (data) => {
|
|
||||||
// 将模型放置到该区域
|
|
||||||
try {
|
|
||||||
const response = await fetch(`http://localhost:3000/api/product-configs/by-sku/${currentText}`);
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.code === 200 && result.data) {
|
|
||||||
console.log('SKU配置数据:', result.data);
|
|
||||||
console.log('关联事件:', result.data.events);
|
|
||||||
placementWall(1);
|
|
||||||
// 使用 for...of 循环以支持 await
|
|
||||||
for (const event of result.data.events) {
|
|
||||||
if (event.event_type === 'change_model') {
|
|
||||||
const { file_url, model_control_type, category } = event.target_data;
|
|
||||||
console.log('替换百叶模型:', event);
|
|
||||||
|
|
||||||
await kernel.model.replace({
|
|
||||||
modelId: category,
|
|
||||||
modelUrl: file_url,
|
|
||||||
modelControlType: model_control_type,
|
|
||||||
drag: {
|
|
||||||
enable: true,
|
|
||||||
axis: 'x',
|
|
||||||
step: 0.1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`百叶模型已替换为 ${currentText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.event_type === 'change_color') {
|
|
||||||
const materialName = event.material_name;
|
|
||||||
const { color, color_map_url, normal_map_url } = event.target_data;
|
|
||||||
console.log('替换百叶模型颜色:', event.target_data);
|
|
||||||
|
|
||||||
kernel.material.apply({
|
|
||||||
target: materialName,
|
|
||||||
albedoColor: color,
|
|
||||||
albedoTexture: color_map_url,
|
|
||||||
normalMap: normal_map_url,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`百叶模型颜色已替换为 ${color}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`未查询到数据`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`查询SKU配置或替换模型失败:`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
164
index.html
164
index.html
@ -455,6 +455,79 @@
|
|||||||
|
|
||||||
import { kernel } from './src/main.ts';
|
import { kernel } from './src/main.ts';
|
||||||
|
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
container: document.querySelector('#renderDom'),
|
||||||
|
modelUrlList: [],
|
||||||
|
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3, background: true },
|
||||||
|
gizmo: {
|
||||||
|
position: true,
|
||||||
|
rotation: true,
|
||||||
|
scale: true
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
enable: true,
|
||||||
|
color: "#2196F3",
|
||||||
|
thickness: 1,
|
||||||
|
occlusionStrength: 0.1,
|
||||||
|
occlusionThreshold: 0.0002
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
kernel.init(config);
|
||||||
|
|
||||||
|
const response = await fetch('http://localhost:3001/api/models/auto-load/list')
|
||||||
|
const data = await response.json()
|
||||||
|
const models = data.data // 这就是模型列表
|
||||||
|
|
||||||
|
models.forEach(model => {
|
||||||
|
console.log(model.placement_zone);
|
||||||
|
if (model.placement_zone) {
|
||||||
|
const { alpha, border_color, color, show_border, thickness, walls } = model.placement_zone
|
||||||
|
kernel.dropZone.setData({
|
||||||
|
|
||||||
|
color: color,
|
||||||
|
alpha: +alpha,
|
||||||
|
thickness: thickness,
|
||||||
|
showBorder: !show_border,
|
||||||
|
borderColor: border_color,
|
||||||
|
walls: walls
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
kernel.model.add({
|
||||||
|
modelId: model.id,
|
||||||
|
modelUrl: model.file_url,
|
||||||
|
modelControlType: model.model_control_type,
|
||||||
|
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
kernel.on('model:load:progress', (data) => {
|
||||||
|
console.log('模型加载事件', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
kernel.on('model:loaded', (data) => {
|
||||||
|
console.log('模型加载完成', data);
|
||||||
|
// 隐藏进度条
|
||||||
|
const progressContainer = document.getElementById('progress-container');
|
||||||
|
if (progressContainer) {
|
||||||
|
progressContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
kernel.on('all:ready', (data) => {
|
||||||
|
console.log('所有模块加载完,', data);
|
||||||
|
kernel.material.apply({
|
||||||
|
target: 'Material__2',
|
||||||
|
attribute: 'alpha',
|
||||||
|
value: 0.5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ========== UI 交互逻辑 ==========
|
// ========== UI 交互逻辑 ==========
|
||||||
|
|
||||||
// 折叠面板切换
|
// 折叠面板切换
|
||||||
@ -799,7 +872,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 初始化放置区域配置数据(只需设置一次)
|
// 初始化放置区域配置数据(只需设置一次)
|
||||||
const initPlacementZoneConfig = (divisions=3) => {
|
const initPlacementZoneConfig = (divisions = 3) => {
|
||||||
|
|
||||||
// 只清除旧的放置区域网格,不清除模型
|
// 只清除旧的放置区域网格,不清除模型
|
||||||
kernel.dropZone.clearZones();
|
kernel.dropZone.clearZones();
|
||||||
@ -950,6 +1023,95 @@
|
|||||||
console.error(`查询SKU配置或替换模型失败:`, error);
|
console.error(`查询SKU配置或替换模型失败:`, error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 存储当前选中的材质名和网格
|
||||||
|
let currentMaterialName = '';
|
||||||
|
let currentPickedMesh = null;
|
||||||
|
|
||||||
|
kernel.on('model:click', (data) => {
|
||||||
|
console.log('模型点击事件', data);
|
||||||
|
console.log('模型控制类型:', data.modelControlType);
|
||||||
|
switch (data.modelControlType) {
|
||||||
|
case "color":
|
||||||
|
// DOM 2D转3D 示例:点击模型时显示信息框
|
||||||
|
if (data.pickedMesh && data.pickedPoint) {
|
||||||
|
const meshName = data.pickedMesh.name;
|
||||||
|
const position = data.pickedPoint; // 使用点击位置的坐标
|
||||||
|
currentMaterialName = data.materialName || ''; // 保存材质名
|
||||||
|
currentPickedMesh = data.pickedMesh; // 保存网格对象
|
||||||
|
|
||||||
|
// 获取已创建的DOM元素
|
||||||
|
const infoDiv = document.getElementById('model-info-box');
|
||||||
|
// 更新信息内容
|
||||||
|
document.getElementById('info-name').textContent = `名称: ${meshName}`;
|
||||||
|
document.getElementById('info-position').textContent = `坐标: [${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}]`;
|
||||||
|
|
||||||
|
// 显示颜色按钮,隐藏旋转按钮
|
||||||
|
document.getElementById('color-buttons').style.display = 'flex';
|
||||||
|
document.getElementById('rotation-buttons').style.display = 'none';
|
||||||
|
|
||||||
|
// 将DOM附加到点击的3D坐标(会自动显示)
|
||||||
|
kernel.domTo3D.attach('model-info', infoDiv, [position.x, position.y, position.z], { x: -2, y: -2 });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "rotation":
|
||||||
|
// 显示旋转控制UI
|
||||||
|
if (data.pickedMesh && data.pickedPoint) {
|
||||||
|
const meshName = data.pickedMesh.name;
|
||||||
|
const position = data.pickedPoint;
|
||||||
|
currentPickedMesh = data.pickedMesh; // 保存网格对象
|
||||||
|
|
||||||
|
// 获取已创建的DOM元素
|
||||||
|
const infoDiv = document.getElementById('model-info-box');
|
||||||
|
// 更新信息内容
|
||||||
|
document.getElementById('info-name').textContent = `名称: ${meshName}`;
|
||||||
|
document.getElementById('info-position').textContent = `坐标: [${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}]`;
|
||||||
|
|
||||||
|
// 显示旋转按钮,隐藏颜色按钮
|
||||||
|
document.getElementById('rotation-buttons').style.display = 'flex';
|
||||||
|
document.getElementById('color-buttons').style.display = 'none';
|
||||||
|
|
||||||
|
// 将DOM附加到点击的3D坐标(会自动显示)
|
||||||
|
kernel.domTo3D.attach('model-info', infoDiv, [position.x, position.y, position.z], { x: -2, y: -2 });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// 暴露到全局,供 index.html 使用
|
||||||
|
window.getCurrentMaterialName = () => currentMaterialName;
|
||||||
|
window.getCurrentPickedMesh = () => currentPickedMesh;
|
||||||
|
|
||||||
|
// 暴露 kernel 到全局,方便调试
|
||||||
|
|
||||||
|
|
||||||
|
kernel.on('hotspot:click', (event) => {
|
||||||
|
console.log('热点被点击:', event);
|
||||||
|
|
||||||
|
const { id, name, payload } = event;
|
||||||
|
|
||||||
|
if (payload && payload.skus && payload.skus.length > 0) {
|
||||||
|
console.log('热点关联的SKU列表:', payload.skus);
|
||||||
|
// 这里可以根据 SKU 列表做进一步处理,比如显示产品信息
|
||||||
|
} else {
|
||||||
|
console.log('该热点没有关联SKU');
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (name === "卷帘门") {
|
||||||
|
// kernel.door.toggle({ upY: 28, downY: 0, speed: 12 });
|
||||||
|
|
||||||
|
// // Y轴剖切,只作用于卷帘门网格,保留下方,剖掉上方
|
||||||
|
// const clipHeight = 28; // 调整这个值找到合适的剖切高度
|
||||||
|
// console.log('设置剖切:', clipHeight);
|
||||||
|
// kernel.clipping.setY(clipHeight, true, ['Box005.001', 'Box006.001']);
|
||||||
|
// }
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
189
index.js
189
index.js
@ -1,189 +0,0 @@
|
|||||||
import { kernel } from './src/main.ts';
|
|
||||||
// import { kernel } from 'https://sdk.zguiy.com/zt/assets/index.js';
|
|
||||||
|
|
||||||
// const config = {
|
|
||||||
// container: document.querySelector('#renderDom'),
|
|
||||||
// modelUrlList: ['/assets/model.glb'],
|
|
||||||
// env: { envPath: '/assets/hdr.env', intensity: 1.2, rotationY: 0.3, background: false },
|
|
||||||
// };
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
container: document.querySelector('#renderDom'),
|
|
||||||
modelUrlList: [],
|
|
||||||
env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3, background: true },
|
|
||||||
gizmo: {
|
|
||||||
position: true,
|
|
||||||
rotation: true,
|
|
||||||
scale: true
|
|
||||||
},
|
|
||||||
outline: {
|
|
||||||
enable: true,
|
|
||||||
color: "#2196F3",
|
|
||||||
thickness: 1,
|
|
||||||
occlusionStrength: 0.1,
|
|
||||||
occlusionThreshold: 0.0002
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
kernel.init(config);
|
|
||||||
|
|
||||||
const response = await fetch('http://localhost:3001/api/models/auto-load/list')
|
|
||||||
const data = await response.json()
|
|
||||||
const models = data.data // 这就是模型列表
|
|
||||||
|
|
||||||
models.forEach(model => {
|
|
||||||
console.log(model.placement_zone);
|
|
||||||
if (model.placement_zone) {
|
|
||||||
const { alpha, border_color, color, show_border, thickness, walls } = model.placement_zone
|
|
||||||
kernel.dropZone.setData({
|
|
||||||
|
|
||||||
color: color,
|
|
||||||
alpha: +alpha,
|
|
||||||
thickness: thickness,
|
|
||||||
showBorder: !show_border,
|
|
||||||
borderColor: border_color,
|
|
||||||
walls: walls
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
kernel.model.add({
|
|
||||||
modelId: model.id,
|
|
||||||
modelUrl: model.file_url,
|
|
||||||
modelControlType: model.model_control_type,
|
|
||||||
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
kernel.on('model:load:progress', (data) => {
|
|
||||||
console.log('模型加载事件', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
kernel.on('model:loaded', (data) => {
|
|
||||||
console.log('模型加载完成', data);
|
|
||||||
// 隐藏进度条
|
|
||||||
const progressContainer = document.getElementById('progress-container');
|
|
||||||
if (progressContainer) {
|
|
||||||
progressContainer.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
kernel.on('all:ready', (data) => {
|
|
||||||
console.log('所有模块加载完,', data);
|
|
||||||
kernel.material.apply({
|
|
||||||
target: 'Material__2',
|
|
||||||
attribute: 'alpha',
|
|
||||||
value: 0.5,
|
|
||||||
});
|
|
||||||
// kernel.hotspot.render([
|
|
||||||
// {
|
|
||||||
// id: "h1",
|
|
||||||
// type: 'hotspot',
|
|
||||||
// name: "卷帘门",
|
|
||||||
// meshName: "Valve_01",
|
|
||||||
// icon: "https://bpic.588ku.com/element_pic/20/06/30/d1046b01afc0b9586844350d131f4daf.jpg!/fw/253/quality/90/unsharp/true/compress/true",
|
|
||||||
// position: [25, 25, 0],
|
|
||||||
// radius: 20,
|
|
||||||
// color: "#21c7ff",
|
|
||||||
// payload: { type: "valve", code: "A" },
|
|
||||||
// },
|
|
||||||
// ]);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// 存储当前选中的材质名和网格
|
|
||||||
let currentMaterialName = '';
|
|
||||||
let currentPickedMesh = null;
|
|
||||||
|
|
||||||
kernel.on('model:click', (data) => {
|
|
||||||
console.log('模型点击事件', data);
|
|
||||||
console.log('模型控制类型:', data.modelControlType);
|
|
||||||
switch (data.modelControlType) {
|
|
||||||
case "color":
|
|
||||||
// DOM 2D转3D 示例:点击模型时显示信息框
|
|
||||||
if (data.pickedMesh && data.pickedPoint) {
|
|
||||||
const meshName = data.pickedMesh.name;
|
|
||||||
const position = data.pickedPoint; // 使用点击位置的坐标
|
|
||||||
currentMaterialName = data.materialName || ''; // 保存材质名
|
|
||||||
currentPickedMesh = data.pickedMesh; // 保存网格对象
|
|
||||||
|
|
||||||
// 获取已创建的DOM元素
|
|
||||||
const infoDiv = document.getElementById('model-info-box');
|
|
||||||
// 更新信息内容
|
|
||||||
document.getElementById('info-name').textContent = `名称: ${meshName}`;
|
|
||||||
document.getElementById('info-position').textContent = `坐标: [${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}]`;
|
|
||||||
|
|
||||||
// 显示颜色按钮,隐藏旋转按钮
|
|
||||||
document.getElementById('color-buttons').style.display = 'flex';
|
|
||||||
document.getElementById('rotation-buttons').style.display = 'none';
|
|
||||||
|
|
||||||
// 将DOM附加到点击的3D坐标(会自动显示)
|
|
||||||
kernel.domTo3D.attach('model-info', infoDiv, [position.x, position.y, position.z], { x: -2, y: -2 });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "rotation":
|
|
||||||
// 显示旋转控制UI
|
|
||||||
if (data.pickedMesh && data.pickedPoint) {
|
|
||||||
const meshName = data.pickedMesh.name;
|
|
||||||
const position = data.pickedPoint;
|
|
||||||
currentPickedMesh = data.pickedMesh; // 保存网格对象
|
|
||||||
|
|
||||||
// 获取已创建的DOM元素
|
|
||||||
const infoDiv = document.getElementById('model-info-box');
|
|
||||||
// 更新信息内容
|
|
||||||
document.getElementById('info-name').textContent = `名称: ${meshName}`;
|
|
||||||
document.getElementById('info-position').textContent = `坐标: [${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}]`;
|
|
||||||
|
|
||||||
// 显示旋转按钮,隐藏颜色按钮
|
|
||||||
document.getElementById('rotation-buttons').style.display = 'flex';
|
|
||||||
document.getElementById('color-buttons').style.display = 'none';
|
|
||||||
|
|
||||||
// 将DOM附加到点击的3D坐标(会自动显示)
|
|
||||||
kernel.domTo3D.attach('model-info', infoDiv, [position.x, position.y, position.z], { x: -2, y: -2 });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// 暴露到全局,供 index.html 使用
|
|
||||||
window.getCurrentMaterialName = () => currentMaterialName;
|
|
||||||
window.getCurrentPickedMesh = () => currentPickedMesh;
|
|
||||||
|
|
||||||
// 暴露 kernel 到全局,方便调试
|
|
||||||
|
|
||||||
|
|
||||||
kernel.on('hotspot:click', (event) => {
|
|
||||||
console.log('热点被点击:', event);
|
|
||||||
|
|
||||||
const { id, name, payload } = event;
|
|
||||||
|
|
||||||
if (payload && payload.skus && payload.skus.length > 0) {
|
|
||||||
console.log('热点关联的SKU列表:', payload.skus);
|
|
||||||
// 这里可以根据 SKU 列表做进一步处理,比如显示产品信息
|
|
||||||
} else {
|
|
||||||
console.log('该热点没有关联SKU');
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (name === "卷帘门") {
|
|
||||||
// kernel.door.toggle({ upY: 28, downY: 0, speed: 12 });
|
|
||||||
|
|
||||||
// // Y轴剖切,只作用于卷帘门网格,保留下方,剖掉上方
|
|
||||||
// const clipHeight = 28; // 调整这个值找到合适的剖切高度
|
|
||||||
// console.log('设置剖切:', clipHeight);
|
|
||||||
// kernel.clipping.setY(clipHeight, true, ['Box005.001', 'Box006.001']);
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
|
|
||||||
window.kernel = kernel;
|
|
||||||
// 添加模型到场景示例
|
|
||||||
// await kernel.model.add({ modelId: '百叶1', modelUrl: 'https://sdk.zguiy.com/resurces/model/百叶1.glb', modelControlType: 'rotation' });
|
|
||||||
|
|
||||||
// 销毁模型
|
|
||||||
// kernel.model.removeByName('car');
|
|
||||||
|
|
||||||
// 替换模型示例
|
|
||||||
// await kernel.model.replace({ modelId: 'car', modelUrl: '/models/new-car.glb', modelControlType: 'color' });
|
|
||||||
|
|
||||||
|
|||||||
0
request.js
Normal file
0
request.js
Normal file
Reference in New Issue
Block a user