Files
DianZhanDemo/app/components/Viewer.vue

267 lines
7.2 KiB
Vue
Raw Normal View History

2025-12-11 01:01:11 +08:00
<!--
* 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>