模型显示
This commit is contained in:
parent
03e4421586
commit
a9ce1a4f77
@ -1,129 +1,228 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="cad-model-viewer">
|
<div class="model-preview" :style="{ width: width, height: height }">
|
||||||
<div ref="containerRef" class="cad-model-viewer__canvas" />
|
<div v-if="loading" class="loading-mask">
|
||||||
<div v-if="loading" class="cad-model-viewer__overlay">
|
|
||||||
<n-spin size="large" />
|
<n-spin size="large" />
|
||||||
<span>模型加载中…</span>
|
<p class="loading-text">模型加载中...</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="error" class="cad-model-viewer__overlay cad-model-viewer__overlay--error">
|
<div ref="canvasRef" class="canvas-container"></div>
|
||||||
<n-empty :description="error" />
|
|
||||||
</div>
|
<div class="cad-model-viewer__toolbar">
|
||||||
<div v-if="!loading && !error" class="cad-model-viewer__toolbar">
|
|
||||||
<n-button size="tiny" quaternary @click="resetView">复位</n-button>
|
<n-button size="tiny" quaternary @click="resetView">复位</n-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup>
|
||||||
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { NButton, NSpin, NEmpty } from 'naive-ui'
|
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
import { STLLoader } from 'three/addons/loaders/STLLoader.js'
|
||||||
import { createGLTFLoader, disposeDracoLoader } from '@/utils/gltfLoader'
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
|
||||||
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps({
|
||||||
/** 远程 glb/gltf 地址(需登录时可带 Token) */
|
modelUrl: {
|
||||||
url?: string
|
type: String,
|
||||||
/** 本地文件(与 url 二选一,file 优先) */
|
},
|
||||||
file?: File | null
|
width: {
|
||||||
}>()
|
type: String,
|
||||||
|
default: '100%'
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: '600px'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const containerRef = ref<HTMLDivElement | null>(null)
|
const emit = defineEmits(['load-success', 'load-error'])
|
||||||
const loading = ref(false)
|
|
||||||
|
const canvasRef = ref(null)
|
||||||
|
const loading = ref(true)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
|
||||||
let renderer: THREE.WebGLRenderer | null = null
|
|
||||||
let scene: THREE.Scene | null = null
|
|
||||||
let camera: THREE.PerspectiveCamera | null = null
|
|
||||||
let controls: OrbitControls | null = null
|
|
||||||
let animationId = 0
|
|
||||||
let modelRoot: THREE.Object3D | null = null
|
|
||||||
let defaultCameraPosition = new THREE.Vector3(2, 2, 2)
|
|
||||||
let defaultTarget = new THREE.Vector3(0, 0, 0)
|
|
||||||
let disposed = false
|
|
||||||
let objectUrls: string[] = []
|
|
||||||
|
|
||||||
function resolveUrl(url: string) {
|
let scene, camera, renderer, controls
|
||||||
if (url.startsWith('http://') || url.startsWith('https://')) return url
|
let animationId = null
|
||||||
if (url.startsWith('/')) return `${window.location.origin}${url}`
|
let currentModel = null
|
||||||
return url
|
let dracoLoader
|
||||||
|
let gltfLoader
|
||||||
|
let defaultCameraPosition = new THREE.Vector3(0,0,9)
|
||||||
|
let defaultTarget = new THREE.Vector3(0,0,0)
|
||||||
|
|
||||||
|
|
||||||
|
function resolveUrl(modelUrl) {
|
||||||
|
if (modelUrl.startsWith('http://') || url.startsWith('https://')) return modelUrl
|
||||||
|
if (modelUrl.startsWith('/')) return `${window.location.origin}${modelUrl}`
|
||||||
|
return modelUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchModelArrayBuffer(url: string): Promise<ArrayBuffer> {
|
const initScene = () => {
|
||||||
const userStore = useUserStore()
|
const container = canvasRef.value
|
||||||
const headers: Record<string, string> = {}
|
|
||||||
if (userStore.token) headers.Authorization = userStore.token
|
|
||||||
|
|
||||||
const res = await fetch(resolveUrl(url), { headers })
|
|
||||||
if (!res.ok) throw new Error(`模型加载失败 (${res.status})`)
|
|
||||||
return res.arrayBuffer()
|
|
||||||
}
|
|
||||||
|
|
||||||
function initScene() {
|
|
||||||
const container = containerRef.value
|
|
||||||
if (!container) return
|
if (!container) return
|
||||||
|
|
||||||
const width = container.clientWidth || 640
|
const w = container.clientWidth
|
||||||
const height = container.clientHeight || 480
|
const h = container.clientHeight
|
||||||
|
|
||||||
scene = new THREE.Scene()
|
scene = new THREE.Scene() //舞台
|
||||||
scene.background = new THREE.Color(0xf0f2f5)
|
scene.background = new THREE.Color('#FFFFFF') //背景色
|
||||||
|
|
||||||
camera = new THREE.PerspectiveCamera(45, width / height, 0.01, 10000)
|
//相机
|
||||||
camera.position.copy(defaultCameraPosition)
|
//决定从哪个角度观察3D世界,常用两种:
|
||||||
|
// 1.PerspectiveCamera 透视相机(人眼视觉 近大远小)
|
||||||
|
// 2.OrthographicCamera 正交相机(人眼视觉 大大缩放)
|
||||||
|
|
||||||
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
|
camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 10000)
|
||||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
// camera = new THREE.OrthographicCamera(45, w / h, 0.1, 10000)
|
||||||
renderer.setSize(width, height)
|
camera.position.set(0, 0, 200)
|
||||||
renderer.outputColorSpace = THREE.SRGBColorSpace
|
|
||||||
container.innerHTML = ''
|
// 大模型性能降级:关闭抗锯齿、限制像素比、关闭阴影
|
||||||
|
// Renderer 渲染器
|
||||||
|
// 连接场景+相机 输出画面到Canvas画布 调用显卡渲染
|
||||||
|
// WebGLRenderer 硬件加速
|
||||||
|
// WebGPURenderer 新一代高性能渲染
|
||||||
|
renderer = new THREE.WebGLRenderer({
|
||||||
|
antialias: false,
|
||||||
|
preserveDrawingBuffer: true,
|
||||||
|
powerPreference:"low-power" //适应手机端 开启兼容低性能WebGL
|
||||||
|
})
|
||||||
|
renderer.setSize(w, h)
|
||||||
|
renderer.setPixelRatio(1) //手机强制1倍像素,避免高DPI爆现存卡顿
|
||||||
|
// renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1))
|
||||||
|
renderer.shadowMap.enabled = false
|
||||||
container.appendChild(renderer.domElement)
|
container.appendChild(renderer.domElement)
|
||||||
|
|
||||||
controls = new OrbitControls(camera, renderer.domElement)
|
// 光照
|
||||||
controls.enableDamping = true
|
const ambientLight = new THREE.AmbientLight(0xffffff, 2.50)//环境光 均匀漫射 无阴影
|
||||||
controls.dampingFactor = 0.08
|
scene.add(ambientLight)
|
||||||
controls.target.copy(defaultTarget)
|
const mainLight = new THREE.DirectionalLight(0xffffff, 1.00)//平行光 太阳光,支持阴影
|
||||||
|
mainLight.position.set(200, 300, 200)
|
||||||
const ambient = new THREE.AmbientLight(0xffffff, 0.75)
|
scene.add(mainLight)
|
||||||
const dir1 = new THREE.DirectionalLight(0xffffff, 1.1)
|
const fillLight = new THREE.DirectionalLight(0xffffff, 0.35)
|
||||||
dir1.position.set(5, 8, 6)
|
fillLight.position.set(-200, -100, -200)
|
||||||
const dir2 = new THREE.DirectionalLight(0xffffff, 0.45)
|
scene.add(fillLight)
|
||||||
dir2.position.set(-4, -2, -5)
|
|
||||||
scene.add(ambient, dir1, dir2)
|
|
||||||
|
|
||||||
const grid = new THREE.GridHelper(10, 20, 0xcccccc, 0xe8e8e8)
|
const grid = new THREE.GridHelper(10, 20, 0xcccccc, 0xe8e8e8)
|
||||||
scene.add(grid)
|
scene.add(grid)
|
||||||
|
|
||||||
animate()
|
|
||||||
|
controls = new OrbitControls(camera, renderer.domElement)
|
||||||
|
controls.enableDamping = true
|
||||||
|
controls.dampingFactor = 0.05
|
||||||
|
controls.enablePan = true
|
||||||
|
controls.minDistance = 1
|
||||||
|
controls.maxDistance = 10000
|
||||||
|
|
||||||
|
// 初始化Draco解码器绑定GLTFLoader
|
||||||
|
dracoLoader = new DRACOLoader()
|
||||||
|
dracoLoader.setDecoderPath('https://unpkg.com/three@0.160.0/examples/jsm/libs/draco/')
|
||||||
|
dracoLoader.setDecoderConfig({ type: 'js' })
|
||||||
|
gltfLoader = new GLTFLoader()
|
||||||
|
gltfLoader.setDRACOLoader(dracoLoader)
|
||||||
|
loadModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
function animate() {
|
const clearOldModel = () => {
|
||||||
animationId = requestAnimationFrame(animate)
|
if (!scene) return
|
||||||
controls?.update()
|
if (currentModel) {
|
||||||
if (renderer && scene && camera) renderer.render(scene, camera)
|
scene.remove(currentModel)
|
||||||
|
currentModel.traverse((child) => {
|
||||||
|
if (child.geometry) child.geometry.dispose()
|
||||||
|
if (child.material) {
|
||||||
|
if (Array.isArray(child.material)) child.material.forEach(m => m.dispose())
|
||||||
|
else child.material.dispose()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
currentModel = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function frameModel(object: THREE.Object3D) {
|
const fitModelToView = (model) => {
|
||||||
if (!camera || !controls) return
|
const box = new THREE.Box3().setFromObject(model)
|
||||||
const box = new THREE.Box3().setFromObject(object)
|
|
||||||
if (box.isEmpty()) return
|
|
||||||
|
|
||||||
const size = box.getSize(new THREE.Vector3())
|
|
||||||
const center = box.getCenter(new THREE.Vector3())
|
const center = box.getCenter(new THREE.Vector3())
|
||||||
const maxDim = Math.max(size.x, size.y, size.z, 0.001)
|
const size = box.getSize(new THREE.Vector3())
|
||||||
const dist = maxDim * 1.8 / Math.tan((camera.fov * Math.PI) / 360)
|
model.position.sub(center)
|
||||||
|
const maxDim = Math.max(size.x, size.y, size.z)
|
||||||
defaultTarget.copy(center)
|
const fov = camera.fov * (Math.PI / 180)
|
||||||
defaultCameraPosition.set(center.x + dist * 0.6, center.y + dist * 0.5, center.z + dist * 0.8)
|
const cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2)) * 2.2
|
||||||
camera.position.copy(defaultCameraPosition)
|
camera.position.set(0, 0, cameraZ)
|
||||||
camera.near = maxDim / 100
|
camera.lookAt(0, 0, 0)
|
||||||
camera.far = maxDim * 100
|
|
||||||
camera.updateProjectionMatrix()
|
camera.updateProjectionMatrix()
|
||||||
controls.target.copy(center)
|
controls.target.set(0, 0, 0)
|
||||||
controls.update()
|
controls.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadModel = () => {
|
||||||
|
|
||||||
|
if (!props.modelUrl || !gltfLoader) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
clearOldModel()
|
||||||
|
const url = props.modelUrl
|
||||||
|
const ext = url.split('.').pop().toLowerCase()
|
||||||
|
|
||||||
|
if (ext === 'stl') {
|
||||||
|
const loader = new STLLoader()
|
||||||
|
|
||||||
|
loader.load(
|
||||||
|
url,
|
||||||
|
(geometry) => {
|
||||||
|
loading.value = false
|
||||||
|
geometry.computeVertexNormals()
|
||||||
|
geometry.center()
|
||||||
|
const material = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xb8bcc2, metalness: 0.25, roughness: 0.75, side: THREE.DoubleSide
|
||||||
|
})
|
||||||
|
const mesh = new THREE.Mesh(geometry, material)
|
||||||
|
currentModel = mesh
|
||||||
|
scene.add(mesh)
|
||||||
|
fitModelToView(mesh)
|
||||||
|
emit('load-success', mesh)
|
||||||
|
},
|
||||||
|
(xhr) => console.log(`STL加载 ${(xhr.loaded/xhr.total*100).toFixed(1)}%`),
|
||||||
|
(err) => { loading.value = false; console.error('STL错误', err); emit('load-error', err) }
|
||||||
|
)
|
||||||
|
} else if (ext === 'glb' || ext === 'gltf') {
|
||||||
|
gltfLoader.load(
|
||||||
|
url,
|
||||||
|
(gltf) => {
|
||||||
|
console.log('✅ 成功进入解析回调,开始处理模型')
|
||||||
|
loading.value = false
|
||||||
|
const model = gltf.scene
|
||||||
|
currentModel = model
|
||||||
|
fitModelToView(model)
|
||||||
|
scene.add(model)
|
||||||
|
emit('load-success', model)
|
||||||
|
},
|
||||||
|
// 打印下载进度,判断是否下载卡住
|
||||||
|
(xhr) => console.log(`GLB下载进度 ${(xhr.loaded/xhr.total*100).toFixed(1)}%`),
|
||||||
|
(err) => {
|
||||||
|
loading.value = false
|
||||||
|
console.error('GLB加载失败完整报错:', err)
|
||||||
|
emit('load-error', err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
loading.value = false
|
||||||
|
emit('load-error', new Error('仅支持 stl / glb / gltf'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
animationId = requestAnimationFrame(animate)
|
||||||
|
controls?.update()
|
||||||
|
renderer?.render(scene, camera)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onWindowResize = () => {
|
||||||
|
if (!camera || !renderer || !canvasRef.value) return
|
||||||
|
const w = canvasRef.value.clientWidth
|
||||||
|
const h = canvasRef.value.clientHeight
|
||||||
|
camera.aspect = w / h
|
||||||
|
camera.updateProjectionMatrix()
|
||||||
|
renderer.setSize(w, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function resetView() {
|
function resetView() {
|
||||||
if (!camera || !controls) return
|
if (!camera || !controls) return
|
||||||
camera.position.copy(defaultCameraPosition)
|
camera.position.copy(defaultCameraPosition)
|
||||||
@ -131,178 +230,59 @@ function resetView() {
|
|||||||
controls.update()
|
controls.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearModel() {
|
|
||||||
if (modelRoot && scene) {
|
|
||||||
scene.remove(modelRoot)
|
|
||||||
modelRoot.traverse((child) => {
|
|
||||||
const mesh = child as THREE.Mesh
|
|
||||||
if (mesh.geometry) mesh.geometry.dispose()
|
|
||||||
if (mesh.material) {
|
|
||||||
const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
|
|
||||||
mats.forEach(m => m.dispose())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
modelRoot = null
|
|
||||||
}
|
|
||||||
objectUrls.forEach(u => URL.revokeObjectURL(u))
|
|
||||||
objectUrls = []
|
|
||||||
}
|
|
||||||
|
|
||||||
function disposeViewer() {
|
watch(() => props.modelUrl, (newVal) => newVal && loadModel(), { immediate: true })
|
||||||
disposed = true
|
|
||||||
cancelAnimationFrame(animationId)
|
|
||||||
clearModel()
|
|
||||||
controls?.dispose()
|
|
||||||
renderer?.dispose()
|
|
||||||
controls = null
|
|
||||||
renderer = null
|
|
||||||
scene = null
|
|
||||||
camera = null
|
|
||||||
disposeDracoLoader()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadFromFile(file: File) {
|
|
||||||
if (disposed) return
|
|
||||||
loading.value = true
|
|
||||||
error.value = ''
|
|
||||||
clearModel()
|
|
||||||
|
|
||||||
const ext = file.name.split('.').pop()?.toLowerCase() ?? ''
|
|
||||||
if (ext !== 'glb' && ext !== 'gltf') {
|
|
||||||
error.value = '仅支持 .glb / .gltf 文件'
|
|
||||||
loading.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!scene) initScene()
|
|
||||||
const loader = createGLTFLoader()
|
|
||||||
|
|
||||||
if (ext === 'glb') {
|
|
||||||
const buffer = await file.arrayBuffer()
|
|
||||||
const gltf = await new Promise<{ scene: THREE.Object3D }>((resolve, reject) => {
|
|
||||||
loader.parse(buffer, '', resolve, reject)
|
|
||||||
})
|
|
||||||
modelRoot = gltf.scene
|
|
||||||
} else {
|
|
||||||
const objectUrl = URL.createObjectURL(file)
|
|
||||||
objectUrls.push(objectUrl)
|
|
||||||
const gltf = await new Promise<{ scene: THREE.Object3D }>((resolve, reject) => {
|
|
||||||
loader.load(objectUrl, (g) => resolve(g), undefined, reject)
|
|
||||||
})
|
|
||||||
modelRoot = gltf.scene
|
|
||||||
}
|
|
||||||
|
|
||||||
scene!.add(modelRoot!)
|
|
||||||
frameModel(modelRoot!)
|
|
||||||
} catch (e) {
|
|
||||||
error.value = ext === 'gltf'
|
|
||||||
? 'gltf 加载失败;若含外部 .bin 请改用单文件 .glb'
|
|
||||||
: (e instanceof Error ? e.message : '模型加载失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadModel(url: string) {
|
|
||||||
if (!url || disposed) return
|
|
||||||
loading.value = true
|
|
||||||
error.value = ''
|
|
||||||
clearModel()
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!scene) initScene()
|
|
||||||
const buffer = await fetchModelArrayBuffer(url)
|
|
||||||
const loader = createGLTFLoader()
|
|
||||||
const gltf = await new Promise<{ scene: THREE.Object3D }>((resolve, reject) => {
|
|
||||||
loader.parse(buffer, '', resolve, reject)
|
|
||||||
})
|
|
||||||
modelRoot = gltf.scene
|
|
||||||
scene!.add(modelRoot)
|
|
||||||
frameModel(modelRoot)
|
|
||||||
} catch (e) {
|
|
||||||
error.value = e instanceof Error ? e.message : '模型加载失败'
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onResize() {
|
|
||||||
const container = containerRef.value
|
|
||||||
if (!container || !renderer || !camera) return
|
|
||||||
const width = container.clientWidth
|
|
||||||
const height = container.clientHeight
|
|
||||||
if (!width || !height) return
|
|
||||||
camera.aspect = width / height
|
|
||||||
camera.updateProjectionMatrix()
|
|
||||||
renderer.setSize(width, height)
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [props.file, props.url] as const,
|
|
||||||
([file, url]) => {
|
|
||||||
if (file) {
|
|
||||||
loadFromFile(file)
|
|
||||||
} else if (url) {
|
|
||||||
loadModel(url)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('resize', onResize)
|
initScene()
|
||||||
|
animate()
|
||||||
|
window.addEventListener('resize', onWindowResize)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', onResize)
|
if (dracoLoader) dracoLoader.dispose()
|
||||||
disposeViewer()
|
cancelAnimationFrame(animationId)
|
||||||
|
window.removeEventListener('resize', onWindowResize)
|
||||||
|
clearOldModel()
|
||||||
|
renderer?.dispose()
|
||||||
|
scene = camera = renderer = controls = dracoLoader = gltfLoader = null
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.cad-model-viewer {
|
.model-preview {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
border: 1px solid #e5e7eb;
|
||||||
height: 100%;
|
border-radius: 4px;
|
||||||
min-height: 420px;
|
|
||||||
background: #f0f2f5;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
background: #f0f2f5;
|
||||||
}
|
}
|
||||||
|
.canvas-container {
|
||||||
.cad-model-viewer__canvas {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 420px;
|
|
||||||
}
|
}
|
||||||
|
.loading-mask {
|
||||||
.cad-model-viewer__canvas :deep(canvas) {
|
|
||||||
display: block;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cad-model-viewer__overlay {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(255,255,255,0.85);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 12px;
|
z-index: 10;
|
||||||
background: rgba(255, 255, 255, 0.85);
|
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
|
.loading-text {
|
||||||
.cad-model-viewer__overlay--error {
|
margin-top: 12px;
|
||||||
background: rgba(255, 255, 255, 0.92);
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cad-model-viewer__toolbar {
|
.cad-model-viewer__toolbar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -14,7 +14,7 @@
|
|||||||
<n-text depth="3">首次查看可能需要 30 秒~数分钟</n-text>
|
<n-text depth="3">首次查看可能需要 30 秒~数分钟</n-text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CadModelViewer v-else-if="previewUrl" :url="previewUrl" class="plm-model-drawer__viewer" />
|
<CadModelViewer v-else-if="previewUrl" :modelUrl="previewUrl" class="plm-model-drawer__viewer" />
|
||||||
|
|
||||||
<n-result
|
<n-result
|
||||||
v-else-if="error"
|
v-else-if="error"
|
||||||
@ -70,8 +70,9 @@ function sleep(ms: number) {
|
|||||||
|
|
||||||
async function fetchPreview(query: PlmModelPreviewQuery): Promise<PlmModelPreviewResult> {
|
async function fetchPreview(query: PlmModelPreviewQuery): Promise<PlmModelPreviewResult> {
|
||||||
const maxAttempts = 90
|
const maxAttempts = 90
|
||||||
for (let i = 0; i < maxAttempts; i++) {
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
const res = await plmModelApi.getPreview(query)
|
const res = await plmModelApi.getPreview(query)
|
||||||
|
|
||||||
if (res.status === 'ready' || res.status === 'failed' || res.status === 'not_found') {
|
if (res.status === 'ready' || res.status === 'failed' || res.status === 'not_found') {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@ -86,7 +87,7 @@ async function open(ctx: PlmModelOpenContext) {
|
|||||||
resultMeta.value = null
|
resultMeta.value = null
|
||||||
previewUrl.value = ''
|
previewUrl.value = ''
|
||||||
error.value = ''
|
error.value = ''
|
||||||
converting.value = false
|
converting.value = true
|
||||||
loading.value = true
|
loading.value = true
|
||||||
visible.value = true
|
visible.value = true
|
||||||
|
|
||||||
@ -98,8 +99,10 @@ async function open(ctx: PlmModelOpenContext) {
|
|||||||
processPlanId: ctx.processPlanId,
|
processPlanId: ctx.processPlanId,
|
||||||
dispatchId: ctx.dispatchId,
|
dispatchId: ctx.dispatchId,
|
||||||
})
|
})
|
||||||
|
|
||||||
resultMeta.value = res
|
resultMeta.value = res
|
||||||
|
|
||||||
|
|
||||||
if (res.status === 'ready' && res.previewUrl) {
|
if (res.status === 'ready' && res.previewUrl) {
|
||||||
previewUrl.value = res.previewUrl
|
previewUrl.value = res.previewUrl
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user