358 lines
13 KiB
JavaScript
358 lines
13 KiB
JavaScript
/* eslint-disable @typescript-eslint/no-require-imports */
|
||
/**
|
||
* 更新演示数据脚本 - 添加真实的3D模型
|
||
* 使用更好的3D模型替换默认的演示模型
|
||
*/
|
||
const DEMO_CONFIRM_TOKEN = 'reset-local-demo'
|
||
let prisma = null
|
||
let pool = null
|
||
|
||
function assertLocalDestructiveDemoAllowed() {
|
||
if (!process.argv.includes(`--confirm=${DEMO_CONFIRM_TOKEN}`)) {
|
||
throw new Error(`拒绝执行 destructive demo seed:必须显式追加 --confirm=${DEMO_CONFIRM_TOKEN}。`)
|
||
}
|
||
if (process.env.NODE_ENV === 'production') {
|
||
throw new Error('拒绝执行 destructive demo seed:NODE_ENV=production。')
|
||
}
|
||
const databaseUrl = process.env.DATABASE_URL || ''
|
||
if (!databaseUrl) {
|
||
throw new Error('拒绝执行 destructive demo seed:DATABASE_URL 未配置。')
|
||
}
|
||
const parsed = new URL(databaseUrl)
|
||
if (!['localhost', '127.0.0.1', '::1'].includes(parsed.hostname)) {
|
||
throw new Error('拒绝执行 destructive demo seed:只允许本机数据库。')
|
||
}
|
||
}
|
||
|
||
function createPrismaClient() {
|
||
const { PrismaClient } = require('@prisma/client')
|
||
const { PrismaPg } = require('@prisma/adapter-pg')
|
||
const { Pool } = require('pg')
|
||
pool = new Pool({ connectionString: process.env.DATABASE_URL })
|
||
prisma = new PrismaClient({ adapter: new PrismaPg(pool) })
|
||
}
|
||
|
||
// 优质的免费3D模型资源
|
||
const QUALITY_MODELS = [
|
||
{
|
||
name: '现代办公椅',
|
||
sku: 'CHAIR-MOD-001',
|
||
category: '办公家具',
|
||
modelUrl: 'https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf',
|
||
thumbnailUrl: 'https://threejs.org/examples/screenshots/misc_controls_orbit.jpg',
|
||
description: '现代简约设计的办公椅,符合人体工程学'
|
||
},
|
||
{
|
||
name: '实木餐桌',
|
||
sku: 'TABLE-WOOD-001',
|
||
category: '餐厅家具',
|
||
modelUrl: 'https://threejs.org/examples/models/gltf/Flamingo.glb',
|
||
thumbnailUrl: 'https://threejs.org/examples/screenshots/webgl_animation_skinning_blending.jpg',
|
||
description: '天然实木制作的餐桌,环保耐用'
|
||
},
|
||
{
|
||
name: '北欧书架',
|
||
sku: 'SHELF-NORDIC-001',
|
||
category: '书房家具',
|
||
modelUrl: 'https://threejs.org/examples/models/gltf/Parrot.glb',
|
||
thumbnailUrl: 'https://threejs.org/examples/screenshots/webgl_animation_skinning_morph.jpg',
|
||
description: '北欧风格设计的多层书架,简洁实用'
|
||
}
|
||
]
|
||
|
||
async function updateDemoModels() {
|
||
assertLocalDestructiveDemoAllowed()
|
||
createPrismaClient()
|
||
console.log('🔄 开始更新演示模型...')
|
||
|
||
try {
|
||
// 清理现有数据
|
||
console.log('🧹 清理现有数据...')
|
||
await prisma.shareLink.deleteMany()
|
||
await prisma.assemblyStep.deleteMany()
|
||
await prisma.panel.deleteMany()
|
||
await prisma.product.deleteMany()
|
||
|
||
// 创建新的产品和数据
|
||
for (let i = 0; i < QUALITY_MODELS.length; i++) {
|
||
const modelData = QUALITY_MODELS[i]
|
||
|
||
console.log(`📦 创建产品: ${modelData.name}`)
|
||
|
||
// 创建产品
|
||
const product = await prisma.product.create({
|
||
data: {
|
||
name: modelData.name,
|
||
sku: modelData.sku,
|
||
category: modelData.category,
|
||
modelUrl: modelData.modelUrl,
|
||
thumbnailUrl: modelData.thumbnailUrl
|
||
}
|
||
})
|
||
|
||
// 为每个产品创建装配步骤
|
||
const steps = generateAssemblySteps(modelData.name, i)
|
||
|
||
for (const stepData of steps) {
|
||
await prisma.assemblyStep.create({
|
||
data: {
|
||
...stepData,
|
||
productId: product.id
|
||
}
|
||
})
|
||
}
|
||
|
||
// 创建板件信息
|
||
const panels = generatePanels(modelData.name, modelData.category)
|
||
|
||
for (const panelData of panels) {
|
||
await prisma.panel.create({
|
||
data: {
|
||
...panelData,
|
||
productId: product.id
|
||
}
|
||
})
|
||
}
|
||
|
||
// 创建分享链接
|
||
const shareToken = `demo${i + 1}${Math.random().toString(36).substr(2, 3)}`
|
||
await prisma.shareLink.create({
|
||
data: {
|
||
productId: product.id,
|
||
token: shareToken,
|
||
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90天后过期
|
||
viewCount: Math.floor(Math.random() * 50) + 10 // 随机访问量
|
||
}
|
||
})
|
||
|
||
console.log(`✅ ${modelData.name} 创建完成 (分享链接: /share/${shareToken})`)
|
||
}
|
||
|
||
console.log('\n🎉 演示模型更新完成!')
|
||
console.log('\n📋 可用的分享链接:')
|
||
|
||
const allShareLinks = await prisma.shareLink.findMany({
|
||
include: {
|
||
product: true
|
||
}
|
||
})
|
||
|
||
allShareLinks.forEach(link => {
|
||
console.log(` ${link.product.name}: http://localhost:3000/share/${link.token}`)
|
||
})
|
||
|
||
console.log('\n📊 更新统计:')
|
||
const stats = await getStats()
|
||
console.log(` 产品: ${stats.products}个`)
|
||
console.log(` 装配步骤: ${stats.assemblySteps}个`)
|
||
console.log(` 板件: ${stats.panels}个`)
|
||
console.log(` 分享链接: ${stats.shareLinks}个`)
|
||
|
||
} catch (error) {
|
||
console.error('❌ 更新演示模型失败:', error)
|
||
throw error
|
||
} finally {
|
||
await prisma?.$disconnect()
|
||
await pool?.end()
|
||
}
|
||
}
|
||
|
||
// 生成装配步骤
|
||
function generateAssemblySteps(productName, index) {
|
||
const stepTemplates = [
|
||
// 办公椅步骤
|
||
[
|
||
{
|
||
order: 1,
|
||
title: '准备工作',
|
||
description: '检查包装内容,确认所有配件齐全',
|
||
partIds: ['base', 'wheels', 'screws'],
|
||
transform: { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } },
|
||
duration: 2000,
|
||
easing: 'power2.inOut'
|
||
},
|
||
{
|
||
order: 2,
|
||
title: '安装轮子',
|
||
description: '将5个轮子插入椅子底座的孔中',
|
||
partIds: ['base', 'wheel1', 'wheel2', 'wheel3', 'wheel4', 'wheel5'],
|
||
transform: { position: { x: 0, y: -0.3, z: 0 }, rotation: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } },
|
||
duration: 3000,
|
||
easing: 'power2.inOut'
|
||
},
|
||
{
|
||
order: 3,
|
||
title: '安装气压杆',
|
||
description: '将气压杆插入底座中心孔',
|
||
partIds: ['base', 'cylinder'],
|
||
transform: { position: { x: 0, y: 0.2, z: 0 }, rotation: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } },
|
||
duration: 2500,
|
||
easing: 'power2.inOut'
|
||
},
|
||
{
|
||
order: 4,
|
||
title: '安装座椅',
|
||
description: '将座椅放置在气压杆上,确保稳固',
|
||
partIds: ['seat', 'cylinder'],
|
||
transform: { position: { x: 0, y: 0.5, z: 0 }, rotation: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } },
|
||
duration: 2500,
|
||
easing: 'power2.inOut'
|
||
},
|
||
{
|
||
order: 5,
|
||
title: '安装靠背',
|
||
description: '将靠背与座椅连接,调整到合适角度',
|
||
partIds: ['seat', 'backrest'],
|
||
transform: { position: { x: 0, y: 0.3, z: -0.2 }, rotation: { x: -0.1, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } },
|
||
duration: 3000,
|
||
easing: 'power2.inOut'
|
||
},
|
||
{
|
||
order: 6,
|
||
title: '最终检查',
|
||
description: '检查所有连接点,测试椅子功能',
|
||
partIds: ['complete'],
|
||
transform: { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: Math.PI * 2, z: 0 }, scale: { x: 1, y: 1, z: 1 } },
|
||
duration: 4000,
|
||
easing: 'power2.inOut'
|
||
}
|
||
],
|
||
// 餐桌步骤
|
||
[
|
||
{
|
||
order: 1,
|
||
title: '准备桌腿',
|
||
description: '取出四条桌腿,检查表面是否完好',
|
||
partIds: ['leg1', 'leg2', 'leg3', 'leg4'],
|
||
transform: { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } },
|
||
duration: 2000,
|
||
easing: 'power2.inOut'
|
||
},
|
||
{
|
||
order: 2,
|
||
title: '安装桌腿',
|
||
description: '将桌腿固定到桌面底部的预留孔位',
|
||
partIds: ['tabletop', 'leg1', 'leg2', 'leg3', 'leg4'],
|
||
transform: { position: { x: 0, y: -0.4, z: 0 }, rotation: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } },
|
||
duration: 4000,
|
||
easing: 'power2.inOut'
|
||
},
|
||
{
|
||
order: 3,
|
||
title: '加固连接',
|
||
description: '使用螺丝加固桌腿与桌面的连接',
|
||
partIds: ['tabletop', 'legs', 'screws'],
|
||
transform: { position: { x: 0, y: -0.2, z: 0 }, rotation: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } },
|
||
duration: 3000,
|
||
easing: 'power2.inOut'
|
||
},
|
||
{
|
||
order: 4,
|
||
title: '翻转桌子',
|
||
description: '小心翻转桌子,使其正面朝上',
|
||
partIds: ['complete'],
|
||
transform: { position: { x: 0, y: 0, z: 0 }, rotation: { x: Math.PI, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } },
|
||
duration: 3500,
|
||
easing: 'power2.inOut'
|
||
}
|
||
],
|
||
// 书架步骤
|
||
[
|
||
{
|
||
order: 1,
|
||
title: '组装侧板',
|
||
description: '将左右两块侧板准备就位',
|
||
partIds: ['side_left', 'side_right'],
|
||
transform: { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } },
|
||
duration: 2000,
|
||
easing: 'power2.inOut'
|
||
},
|
||
{
|
||
order: 2,
|
||
title: '安装层板',
|
||
description: '依次安装各层隔板,从下往上',
|
||
partIds: ['side_left', 'side_right', 'shelf1', 'shelf2', 'shelf3'],
|
||
transform: { position: { x: 0, y: 0.1, z: 0 }, rotation: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } },
|
||
duration: 4000,
|
||
easing: 'power2.inOut'
|
||
},
|
||
{
|
||
order: 3,
|
||
title: '安装背板',
|
||
description: '将背板固定到书架后方',
|
||
partIds: ['frame', 'backpanel'],
|
||
transform: { position: { x: 0, y: 0, z: -0.1 }, rotation: { x: 0, y: 0, z: 0 }, scale: { x: 1, y: 1, z: 1 } },
|
||
duration: 3000,
|
||
easing: 'power2.inOut'
|
||
},
|
||
{
|
||
order: 4,
|
||
title: '最终调整',
|
||
description: '检查书架稳定性,调整水平',
|
||
partIds: ['complete'],
|
||
transform: { position: { x: 0, y: 0, z: 0 }, rotation: { x: 0, y: Math.PI / 4, z: 0 }, scale: { x: 1, y: 1, z: 1 } },
|
||
duration: 2500,
|
||
easing: 'power2.inOut'
|
||
}
|
||
]
|
||
]
|
||
|
||
return stepTemplates[index] || stepTemplates[0]
|
||
}
|
||
|
||
// 生成板件信息
|
||
function generatePanels(productName, category) {
|
||
const panelTemplates = {
|
||
'办公家具': [
|
||
{ code: 'A01', name: '座椅面板', gltfNodeName: 'seat', length: 45.0, width: 40.0, thickness: 3.0 },
|
||
{ code: 'A02', name: '靠背面板', gltfNodeName: 'backrest', length: 40.0, width: 50.0, thickness: 2.5 },
|
||
{ code: 'B01', name: '椅腿组件', gltfNodeName: 'legs', length: 60.0, width: 60.0, thickness: 45.0 },
|
||
{ code: 'C01', name: '气压杆', gltfNodeName: 'cylinder', length: 5.0, width: 5.0, thickness: 20.0 }
|
||
],
|
||
'餐厅家具': [
|
||
{ code: 'T01', name: '桌面板', gltfNodeName: 'tabletop', length: 120.0, width: 80.0, thickness: 3.0 },
|
||
{ code: 'T02', name: '桌腿(前左)', gltfNodeName: 'leg1', length: 5.0, width: 5.0, thickness: 72.0 },
|
||
{ code: 'T03', name: '桌腿(前右)', gltfNodeName: 'leg2', length: 5.0, width: 5.0, thickness: 72.0 },
|
||
{ code: 'T04', name: '桌腿(后左)', gltfNodeName: 'leg3', length: 5.0, width: 5.0, thickness: 72.0 },
|
||
{ code: 'T05', name: '桌腿(后右)', gltfNodeName: 'leg4', length: 5.0, width: 5.0, thickness: 72.0 }
|
||
],
|
||
'书房家具': [
|
||
{ code: 'S01', name: '左侧板', gltfNodeName: 'side_left', length: 30.0, width: 180.0, thickness: 2.0 },
|
||
{ code: 'S02', name: '右侧板', gltfNodeName: 'side_right', length: 30.0, width: 180.0, thickness: 2.0 },
|
||
{ code: 'S03', name: '层板1', gltfNodeName: 'shelf1', length: 80.0, width: 28.0, thickness: 2.0 },
|
||
{ code: 'S04', name: '层板2', gltfNodeName: 'shelf2', length: 80.0, width: 28.0, thickness: 2.0 },
|
||
{ code: 'S05', name: '层板3', gltfNodeName: 'shelf3', length: 80.0, width: 28.0, thickness: 2.0 },
|
||
{ code: 'S06', name: '背板', gltfNodeName: 'backpanel', length: 80.0, width: 180.0, thickness: 0.5 }
|
||
]
|
||
}
|
||
|
||
return panelTemplates[category] || panelTemplates['办公家具']
|
||
}
|
||
|
||
// 获取统计信息
|
||
async function getStats() {
|
||
const [products, assemblySteps, panels, shareLinks] = await Promise.all([
|
||
prisma.product.count(),
|
||
prisma.assemblyStep.count(),
|
||
prisma.panel.count(),
|
||
prisma.shareLink.count()
|
||
])
|
||
|
||
return { products, assemblySteps, panels, shareLinks }
|
||
}
|
||
|
||
// 运行脚本
|
||
if (require.main === module) {
|
||
updateDemoModels()
|
||
.then(() => {
|
||
console.log('\n✨ 演示模型更新完成!现在可以体验更丰富的3D展示效果了!')
|
||
process.exit(0)
|
||
})
|
||
.catch((error) => {
|
||
console.error('脚本执行失败:', error)
|
||
process.exit(1)
|
||
})
|
||
}
|
||
|
||
module.exports = { updateDemoModels }
|