Files
DianZhanDemo/app/components/Viewer.vue
ch197511161 908b4361ed init3
2025-12-11 01:01:11 +08:00

267 lines
7.2 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
* File: \src\components\Viewer.vue
* Project: viewer_for_ruwei
* File Created: Tuesday, 10th September 2024 10:44:34 am
* Author: Sun Jie (j.sun@supreium.com)
* -----
* Last Modified: Tuesday, 10th September 2024 10:44:35 am
* Modified By: Sun Jie (j.sun@supreium.com)
* -----
* Description: 3D 网格查看器组件支持属性可视化切换
* -----
* Copyright (c) 2024 Supreium Co., Ltd , All Rights Reserved.
-->
<template>
<div class="viewer-wrapper">
<!-- 顶部属性切换工具栏 -->
<div class="viewer-toolbar">
<el-radio-group
v-model="selected"
size="small"
class="property-toggle-group"
v-if="properties && properties.length > 0"
>
<el-radio-button
v-for="item in properties"
:key="item"
:value="item"
>
{{ item }}
</el-radio-button>
</el-radio-group>
<!-- 视图控制按钮 -->
<div class="view-controls">
<el-tooltip content="适应视图" placement="bottom">
<button class="control-btn" @click="fitAll">
<el-icon><FullScreen /></el-icon>
</button>
</el-tooltip>
<el-tooltip content="等轴视图" placement="bottom">
<button class="control-btn" @click="viewAt">
<el-icon><View /></el-icon>
</button>
</el-tooltip>
</div>
</div>
<!-- 3D 渲染容器 -->
<div class="viewer-container" ref="renderDiv">
<!-- 加载状态指示器 -->
<div v-if="isLoading" class="loading-overlay">
<el-icon class="is-loading loading-icon"><Loading /></el-icon>
<span class="loading-text">LOADING MODEL...</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { FullScreen, Loading, View } from '@element-plus/icons-vue';
import axios from 'axios';
import { onMounted, onUnmounted, ref, watch } from 'vue';
import { ViewHelper } from '../utils/viewHelper';
// 定义组件属性,默认使用 /example.json 作为数据源
const props = withDefaults(defineProps<{
dataPath?: string;
}>(), {
dataPath: '/example.json'
});
const renderDiv = ref<HTMLDivElement>();
const resizeListener = ref<Function[]>([]);
const destroyFunctions = ref<Function[]>([]);
const properties = ref<string[]>();
const selected = ref<string>("");
const isLoading = ref(true);
// 窗口尺寸调整回调
const resizeCallback = () => resizeListener.value.forEach((callback) => callback());
// 监听容器尺寸变化
const onDivResize = (currentDiv: HTMLDivElement, callback: ResizeObserverCallback) => {
resizeListener.value.push(callback);
const resizeObserver = new ResizeObserver(callback);
resizeObserver.observe(currentDiv!);
destroyFunctions.value.push(() => resizeObserver?.disconnect());
};
// 初始化视图
const init = async () => {
window.addEventListener("resize", resizeCallback);
await ViewHelper.instance.initView(renderDiv.value!);
onDivResize(renderDiv.value!, () => ViewHelper.instance.viewResize(renderDiv.value!.clientWidth, renderDiv.value!.clientHeight));
};
onMounted(async () => {
await init();
try {
// 加载网格数据
const mesh = (await axios.get(props.dataPath)).data;
// 创建视图对象
ViewHelper.instance.loadMesh(mesh.mesh, mesh.properties);
/**
* 如需过滤属性可使用:
* ViewHelper.instance.loadMesh(mesh.mesh, (mesh.properties as MeshPropertyType[]).filter((item) => item.name === "vonMises" || item.name === "displacement-magnitude"));
*/
// 设置等轴视图并适应
ViewHelper.instance.isometricView();
ViewHelper.instance.fitAll();
// 获取可用属性列表
properties.value = ViewHelper.instance.properties;
// 激活第一个属性,触发 watch
if (properties.value && properties.value.length > 0) {
selected.value = properties.value[0];
}
// 注册清理回调
destroyFunctions.value.push(() => ViewHelper.instance.clear());
} catch (error) {
console.error('加载模型数据失败:', error);
} finally {
isLoading.value = false;
}
});
onUnmounted(() => {
window.removeEventListener("resize", resizeCallback);
destroyFunctions.value.forEach((fun) => fun());
});
// 监听属性选择变化
watch(selected, () => selected.value !== "" && ViewHelper.instance.activeProperty(selected.value))
// 适应视图
const fitAll = () => ViewHelper.instance.fitAll();
// 等轴视图
const viewAt = () => ViewHelper.instance.isometricView();
</script>
<style lang="css" scoped>
/* 查看器容器 */
.viewer-wrapper {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: #0b1120;
border: 1px solid rgba(6, 182, 212, 0.2);
border-radius: 4px;
overflow: hidden;
}
/* 工具栏 */
.viewer-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: linear-gradient(180deg, rgba(15, 23, 42, 0.95) 0%, rgba(11, 17, 32, 0.9) 100%);
border-bottom: 1px solid rgba(6, 182, 212, 0.15);
gap: 16px;
}
/* 视图控制按钮组 */
.view-controls {
display: flex;
gap: 8px;
}
.control-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 6px;
background: rgba(15, 23, 42, 0.8);
border: 1px solid rgba(34, 211, 238, 0.3);
color: #22d3ee;
cursor: pointer;
transition: all 0.3s ease;
}
.control-btn:hover {
background: rgba(6, 182, 212, 0.2);
box-shadow: 0 0 12px rgba(6, 182, 212, 0.4);
transform: scale(1.05);
border-color: #22d3ee;
}
.control-btn:active {
transform: scale(0.95);
}
/* 3D 渲染区域 */
.viewer-container {
flex: 1;
position: relative;
background: radial-gradient(ellipse at center, #0f172a 0%, #0b1120 100%);
}
/* 加载状态 */
.loading-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(11, 17, 32, 0.9);
backdrop-filter: blur(4px);
z-index: 10;
}
.loading-icon {
font-size: 32px;
color: #22d3ee;
margin-bottom: 12px;
}
.loading-text {
color: rgba(34, 211, 238, 0.8);
font-size: 12px;
letter-spacing: 0.2em;
font-family: monospace;
}
/* 属性切换 Toggle Group 样式 */
:deep(.property-toggle-group .el-radio-button__inner) {
background-color: transparent;
border: 1px solid rgba(34, 211, 238, 0.2);
color: #94a3b8;
font-family: monospace;
font-size: 12px;
transition: all 0.3s ease;
padding: 6px 14px;
}
:deep(.property-toggle-group .el-radio-button__original-radio:checked + .el-radio-button__inner) {
background-color: rgba(6, 182, 212, 0.15);
border-color: #06b6d4;
color: #22d3ee;
box-shadow: 0 0 10px rgba(6, 182, 212, 0.3);
}
:deep(.property-toggle-group .el-radio-button:first-child .el-radio-button__inner) {
border-radius: 4px 0 0 4px;
}
:deep(.property-toggle-group .el-radio-button:last-child .el-radio-button__inner) {
border-radius: 0 4px 4px 0;
}
:deep(.property-toggle-group .el-radio-button__inner:hover) {
color: #22d3ee;
background-color: rgba(34, 211, 238, 0.05);
}
/* 处理单个按钮时的圆角 */
:deep(.property-toggle-group .el-radio-button:only-child .el-radio-button__inner) {
border-radius: 4px;
}
</style>