Three.js基础功能学习十四:智能黑板实现实例一

使用Vue3+Elementplus+Threejs实现智能黑板,实现基础的几何图形、三维图形、手写板的元素动态绘制效果,并实现数学、物理、化学、地理、生物等课程常见的图形元素及函数图形等。

源码下载地址: 点击下载
效果演示:









一、学习视频

https://www.bilibili.com/video/BV1JT69BUEdD/

二、项目创建

使用vue3搭建项目框架,引入three.js实现三维效果。

2.1 项目创建

官网: https://cn.vuejs.org/guide/introduction
命令: npm create vue@latest

2.2 三维引入

官网: https://threejs.org/
命令: npm install three

三、项目结构

项目结构如下:

四、项目基础框架

4.1 基础框架

  1. main.ts
javascript 复制代码
import { createApp } from 'vue'
import { createPinia } from 'pinia'

import App from './App.vue'
import router from './router'

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// if you just want to import css
import 'element-plus/theme-chalk/dark/css-vars.css'
import zhCn from 'element-plus/es/locale/lang/zh-cn'

// 如果您正在使用CDN引入,请删除下面一行。
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

import SvgIcon from './components/SvgIcon.vue'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.use(ElementPlus,{
locale: zhCn,
})

for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}
app.component("SvgIcon", SvgIcon)

app.mount('#app')
  1. App.vue
javascript 复制代码
<script setup lang="ts"></script>

<template>
  <RouterView></RouterView>
</template>

<style >
@import './assets/main.css';
</style>
  1. HomeView.vue
javascript 复制代码
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import D3  from "./d3"

import TopToolBar from "./components/toolbar/top.vue"
import RightToolBar from "./components/toolbar/right.vue"
import BottomToolBar from "./components/toolbar/bottom.vue"
import LeftToolBar from "./components/toolbar/left.vue"
import FreeToolBar from "./components/toolbar/free.vue"
import { useConfigStore } from '@/stores/config';
import { useStateStore } from '@/stores/state';
import { OprationModes } from '@/model/OprationModes';
import { useToolStore } from '@/stores/tool';

//三维容器的dom元素
const d3 = ref();

//鼠标状态
const cursor = ref(useStateStore().cursor);

const tools = useToolStore();

//右侧
const rightToolBar = ref();

/**
 * 监听场景的参数
 */
watch(()=>useConfigStore().d3Config.scene.background,(newVal,oldVal)=>{
  D3.setBackground(newVal);
})
watch(()=>useConfigStore().d3Config.scene.texture,(newVal,oldVal)=>{
  if(newVal){
    D3.setSceneTexture(newVal);
  }else{
    D3.setBackground(useConfigStore().d3Config.scene.background);
  }
})
watch(()=>useConfigStore().d3Config.scene.environment,(newVal,oldVal)=>{
  D3.setSceneEnvironment(newVal);
})
watch(()=>useConfigStore().d3Config.scene.backgroundBlurriness,(newVal,oldVal)=>{
  D3.setBackgroundBlurriness(newVal);
})

/**
 * 灯光配置
 */
watch(()=>useConfigStore().d3Config.light.color,(newVal,oldVal)=>{
  D3.setLightColor(newVal);
})
watch(()=>useConfigStore().d3Config.light.intensity,(newVal,oldVal)=>{
  D3.setLightIntensity(newVal);
})

/**
 * 监听摄像头的参数
 */
watch(()=>useConfigStore().d3Config.camera.fov,(newVal,oldVal)=>{
  D3.setCameraFov(newVal);
})
watch(()=>useConfigStore().d3Config.camera.far,(newVal,oldVal)=>{
  D3.setCameraFar(newVal);
})
watch(()=>useConfigStore().d3Config.camera.near,(newVal,oldVal)=>{
  D3.setCameraNear(newVal);
})
watch(()=>useConfigStore().d3Config.camera.zoom,(newVal,oldVal)=>{
  D3.setCameraZoom(newVal);
})
watch(()=>useConfigStore().d3Config.camera.position,(newVal,oldVal)=>{
  // D3.setCameraPosition(newVal);
},{deep:true})
/**
 * 手写相关参数的监听
 */
watch(()=>useConfigStore().d3Config.handWriting.planeColor,(newVal,oldVal)=>{
  D3.setHandWritingPlaneColor(newVal);
})
watch(()=>useConfigStore().d3Config.handWriting.planeOpacity,(newVal,oldVal)=>{
  D3.setHandWritingPlaneOpacity(newVal);
})
watch(()=>useConfigStore().d3Config.handWriting.planeTexture,(newVal,oldVal)=>{
  if(newVal){
    D3.setHandWritingPlaneTexture(newVal);
  }else{
    D3.setHandWritingPlaneColor(useConfigStore().d3Config.handWriting.planeColor);
  }
})
watch(()=>useConfigStore().d3Config.handWriting.line.color,(newVal,oldVal)=>{
  D3.setHandWritingLineColor(newVal);
})
watch(()=>useConfigStore().d3Config.handWriting.line.width,(newVal,oldVal)=>{
  D3.setHandWritingLineWidth(newVal);
})
watch(()=>useStateStore().cursor,(newVal,oldVal)=>{
  cursor.value = newVal;
})

/**
 * 板擦的变动事件
 */
watch(()=>useConfigStore().d3Config.eraser.color,(newVal,oldVal)=>{
  D3.setEraserColor(newVal);
})
watch(()=>useConfigStore().d3Config.eraser.radius,(newVal,oldVal)=>{
  D3.setEraserRadius(newVal);
})


/**
 * 物体配置属性的变动事件
 */
watch(()=>useConfigStore().d3Config.mesh.d2PlaneColor,(newVal,oldVal)=>{
  D3.setMeshD2PlanColor(newVal);
})
watch(()=>useConfigStore().d3Config.mesh.d2PlaneOpacity,(newVal,oldVal)=>{
  D3.setMeshD2PlanOpacity(newVal);
})
watch(()=>useConfigStore().d3Config.mesh.d2PlaneTexture,(newVal,oldVal)=>{
  if(newVal){
    D3.setMeshD2PlanTexture(newVal);
  }else{
    D3.setMeshD2PlanColor(useConfigStore().d3Config.mesh.d2PlaneColor);
  }
})
watch(()=>useConfigStore().d3Config.mesh.line.color,(newVal,oldVal)=>{
  D3.setMeshLineColor(newVal);
})
watch(()=>useConfigStore().d3Config.mesh.line.width,(newVal,oldVal)=>{
  D3.setMeshLineWidth(newVal);
})
watch(()=>useConfigStore().d3Config.mesh.fill.type,(newVal,oldVal)=>{
  D3.setMeshFillType(newVal);
})
watch(()=>useConfigStore().d3Config.mesh.fill.color,(newVal,oldVal)=>{
  D3.setMeshFillColor(newVal);
})
watch(()=>useConfigStore().d3Config.mesh.fill.texture,(newVal,oldVal)=>{
  // TODO 待处理
})
watch(()=>useConfigStore().d3Config.mesh.fill.customer,(newVal,oldVal)=>{
  // TODO 待处理
})

//模式监听
watch(()=>useStateStore().currentMode,(newVal,oldVal)=>{
  if(OprationModes.D2 == newVal){
    let item = tools.getItemByKey("write");
    if(item){
      item.selected = false;
    }
    
    item = tools.getItemByKey("d3");
    if(item){
      item.selected = false;
    }
    
    item = tools.getItemByKey("d2");
    if(item){
      item.selected = true;
    }

    rightToolBar.value.removeTab("黑板擦");
    rightToolBar.value.removeTab("手写板");
  }else if(OprationModes.D3 == newVal){
    let item = tools.getItemByKey("d2");
    if(item){
      item.selected = false;
    }
    
    item = tools.getItemByKey("write");
    if(item){
      item.selected = false;
    }

    item = tools.getItemByKey("d3");
    if(item){
      item.selected = true;
    }

    rightToolBar.value.removeTab("黑板擦");
    rightToolBar.value.removeTab("手写板");
  }else if(OprationModes.HandWriting == newVal){
    let item = tools.getItemByKey("d2");
    if(item){
      item.selected = false;
    }

    item = tools.getItemByKey("d3");
    if(item){
      item.selected = false;
    }

    item = tools.getItemByKey("write");
    if(item){
      item.selected = true;
    }

    item = tools.getItemByKey("OrbitControls");
    if(item){
      item.selected = false;
    }
  }
})

onMounted(()=>{
  D3.init(d3.value,useConfigStore().d3Config);
})
onBeforeUnmount(()=>{
  D3.dispose();
})
</script>

<template>
  <div class="container" >
    <div class="d3" ref="d3"></div>
    <TopToolBar ></TopToolBar>
    <RightToolBar ref="rightToolBar"></RightToolBar>
    <BottomToolBar ></BottomToolBar>
    <LeftToolBar ></LeftToolBar>
    <FreeToolBar ></FreeToolBar>
  </div>
</template>

<style lang="less" scoped>
  .container{
    width:100vw;
    height: 100vh;
    cursor: v-bind(cursor);
    user-select: none;
        
    .d3{
      width:100vw;
      height: 100vh;
    }
  }
</style>
  1. 路由:router/index.ts
javascript 复制代码
import { createRouter, createWebHistory } from 'vue-router'

import HomeView from '@/views/HomeView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [{
    name:"home",
    path:'/',
    component:HomeView
  }],
})

export default router
  1. 状态存储
    config.ts
javascript 复制代码
import { ref, computed, reactive } from 'vue'
import { defineStore } from 'pinia'
import { FillType, type D3Config } from '@/model/D3Config';
import * as THREE from "three"
/**
 * 系统的全局配置
 */
export const useConfigStore = defineStore('config', () => {

  //三维相关的配置信息
  const d3Config = reactive<D3Config>({
    maxSize:8,
    camera:{
        fov:75,
        near:0.001,
        far:1000,
        zoom:1,
        position:new THREE.Vector3(0,0,10)
    },
    scene:{
        background:new THREE.Color("#010c02"),
        texture:null,
        environment:null,
        backgroundBlurriness:0
    },
    light:{
        intensity:1,
        color:new THREE.Color("#ffffff")
    },
    handWriting:{
      planeColor:new THREE.Color(0x000055),
      planeOpacity:1,
      planeTexture:null,
      line:{
        width:5,
        color:new THREE.Color(0xffffff)
      }
    },
    eraser:{
      color:new THREE.Color(0xffff00),
      radius:1
    },
    mesh:{
        //二维时候的背景板的颜色
        d2PlaneColor:new THREE.Color(0x000055),
        d2PlaneOpacity:1,
        d2PlaneTexture:null,
        //物体的线条颜色
        line:{
            width:5,
            color:new THREE.Color(0xffffff)
        },
        //物体的填充颜色
        fill:{
            type:FillType.Color,
            color:new THREE.Color(0xffffff),
            texture:null,
            customer:null
        }
    }
  })

  //顶部按钮是否自动隐藏
  const topBarVisabled = ref(false);
  
  return { d3Config,topBarVisabled }
})

state.ts

javascript 复制代码
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import {MouseStatus} from '@/model/MouseStatus';
import type D2Mesh from '@/views/d3/mesh/D2Mesh';
import {OprationModes} from '@/model/OprationModes';

/**
 * 当前的展示状态
 */
export const useStateStore = defineStore('state', () => {
  //当前选中的顶部分类math physics geography chemistry biology
  const currentTop = ref("biology");

  //鼠标的状态
  const cursor = ref("url(/images/cricle.png), auto;");

  //当前选中的对象
  const currentMesh = ref<D2Mesh|null>(null);

  //当前三维操作状态
  const currentStatus = ref<MouseStatus>(MouseStatus.None);

  //当前的操作模式
  const currentMode = ref<OprationModes>(OprationModes.D3);
  
  return { currentTop,cursor,currentMesh ,currentStatus,currentMode}
})

texture.ts

javascript 复制代码
import { ref, computed, reactive } from 'vue'
import { defineStore } from 'pinia'
import {MouseStatus} from '@/model/MouseStatus';
import type D2Mesh from '@/views/d3/mesh/D2Mesh';
import type { OptionData } from '@/model/OptionData';

/**
 * 当前的展示状态
 */
export const useTextureStore = defineStore('texture', () => {
  //当前选中的顶部分类
  const textures = reactive<OptionData[]>([{
    value:'',
    label:'无纹理'
  },{
    value:new URL("@/assets/texture/texture-1.png", import.meta.url).href,
    label:'纹理一'
  },{
    value:new URL("@/assets/texture/texture-2.png", import.meta.url).href,
    label:'纹理二'
  },{
    value:new URL("@/assets/texture/texture-3.png", import.meta.url).href,
    label:'纹理三'
  },{
    value:new URL("@/assets/texture/texture-4.png", import.meta.url).href,
    label:'纹理四'
  },{
    value:new URL("@/assets/texture/texture-5.png", import.meta.url).href,
    label:'纹理五'
  },{
    value:new URL("@/assets/texture/texture-6.png", import.meta.url).href,
    label:'纹理六'
  },{
    value:new URL("@/assets/texture/texture-7.png", import.meta.url).href,
    label:'纹理七'
  },{
    value:new URL("@/assets/texture/texture-8.png", import.meta.url).href,
    label:'纹理八'
  },{
    value:new URL("@/assets/texture/texture-9.png", import.meta.url).href,
    label:'纹理九'
  },{
    value:new URL("@/assets/texture/texture-10.png", import.meta.url).href,
    label:'纹理十'
  },{
    value:new URL("@/assets/texture/texture-bg.png", import.meta.url).href,
    label:'纹理十二'
  }]);

  return { textures}
})

tool.ts

javascript 复制代码
import { reactive } from 'vue'
import { defineStore } from 'pinia'
import curriculums from '@/common/tools/curriculums'
import commonTools from '@/common/tools/commonTools'
import mathTools,{initTools as MathInitTools} from '@/common/tools/mathTools'
import routineTools from '@/common/tools/routineTools'
import biologyTools,{initTools as BiologyInitTools} from '@/common/tools/biologyTools'
import geographyTools,{initTools as GeographyInitTools} from '@/common/tools/geographyTools'
import chemistryTools,{initTools as ChemistryInitTools} from '@/common/tools/chemistryTools'
import physicsTools,{initTools as PhysicsInitTools} from '@/common/tools/physicsTools'
import type { Tool } from '@/model/ToolItem/Tool'

/**
 * 系统的工具按钮
 */
export const useToolStore = defineStore('tool', () => {

    //工具按钮信息
    const tools = reactive<Tool>({
        top: [...curriculums],
        left: [...mathTools,...biologyTools,...chemistryTools,...geographyTools,...physicsTools],
        right: [],
        bottom: [...commonTools],
        free: [...routineTools()],
    })

    /**
     * 根据标识以及命令获取对应的工具按钮
     * @param key 
     */
    const getItemByKey = (key: string) => {
        let result = tools.top.find(item => item.key === key);
        if (!result) {
            result = tools.right.find(item => item.key === key);
        }
        if (!result) {
            result = tools.bottom.find(item => item.key === key);
        }
        if (!result) {
            result = tools.left.find(item => item.key === key);
        }
        return result;
    }

    /**
     * 根据标识以及命令获取对应的工具按钮
     * @param key 
     * @param commond 
     */
    const getItemByKeyAndCommond = (key: string, commond: string) => {
        let result = tools.top.find(item => item.key === key && item.commond === commond);
        if (!result) {
            result = tools.right.find(item => item.key === key && item.commond === commond);
        }
        if (!result) {
            result = tools.bottom.find(item => item.key === key && item.commond === commond);
        }
        if (!result) {
            result = tools.left.find(item => item.key === key && item.commond === commond);
        }
        return result;
    }

    const initItemByKey = (key:String)=>{
        switch(key){
            case 'math':
                MathInitTools();
            case 'physics':
                PhysicsInitTools();
            case 'geography':
                GeographyInitTools();
            case 'chemistry':
                ChemistryInitTools();
            case 'biology':
                BiologyInitTools();
        }
    }

    return { tools, getItemByKey, getItemByKeyAndCommond ,initItemByKey}
})
  1. 工具方法
    CommondHandle.ts
javascript 复制代码
import { COMMOND_TYPE_2D_MESH, COMMOND_TYPE_3D_MESH, COMMOND_TYPE_CHANGE_MODEL, COMMOND_TYPE_D2_MODEL, COMMOND_TYPE_D3_HELP, COMMOND_TYPE_D3_MODEL, COMMOND_TYPE_D3_OP, COMMOND_TYPE_OPEN_CONFIG_DIALOG, COMMOND_TYPE_SELF_WRITE } from "@/common/Constant";
import { OprationModes } from "@/model/OprationModes";
import type { ToolBarItem } from "@/model/ToolBartem";
import { useStateStore } from "@/stores/state";
import EventBus from "@/utils/EventBus"

/**
 * 点击工具栏的处理方法
 * @param item 
 */
export const  commond = (item:ToolBarItem)=>{
    if(!item || !item.commond){
        return;
    }
    switch (item.commond){
        case COMMOND_TYPE_CHANGE_MODEL://切换课程
            useStateStore().currentTop = item.key;//更新当前的顶部工具按钮
            EventBus.emit(COMMOND_TYPE_CHANGE_MODEL,item);
        break;
        case COMMOND_TYPE_OPEN_CONFIG_DIALOG://打开配置弹框
            EventBus.emit(COMMOND_TYPE_OPEN_CONFIG_DIALOG,item);
        break;
        case COMMOND_TYPE_SELF_WRITE://是否开启手写
            EventBus.emit(COMMOND_TYPE_SELF_WRITE,item);
        break;
        case COMMOND_TYPE_D3_HELP://三维辅助对象的启听
            EventBus.emit(COMMOND_TYPE_D3_HELP,item);
        break;
        case COMMOND_TYPE_D3_OP://三维的操作
            EventBus.emit(COMMOND_TYPE_D3_OP,item);
        break;
        case COMMOND_TYPE_2D_MESH://二维物体
            EventBus.emit(COMMOND_TYPE_2D_MESH,item);
        break;
        case COMMOND_TYPE_3D_MESH://三维物体的绘制
            EventBus.emit(COMMOND_TYPE_3D_MESH,item);
        break;
        case COMMOND_TYPE_D2_MODEL://二维模式
            EventBus.emit(COMMOND_TYPE_D2_MODEL,item);
        break;
        case COMMOND_TYPE_D3_MODEL://三维模式
            EventBus.emit(COMMOND_TYPE_D3_MODEL,item);
        break;
        default :
        break;
    }
}

EventBus.ts

javascript 复制代码
// event-bus.js
import mitt from 'mitt';

// 创建一个 mitt 实例并导出
const emitter = mitt();

export default emitter;

Utils.ts

javascript 复制代码
import type { ToolBarItem } from "@/model/ToolBartem";

/**
 * 切换工具栏的选中状态
 * @param tools 工具栏
 * @param item_ 要变动的工具栏
 * @param commond 指令
 */
export const changeToolItemSelected = (tools:ToolBarItem[],item_:ToolBarItem,commond:String)=>{
    let toolItem = tools.find(item=>item.commond === commond && (item_ as ToolBarItem).key === item.key);
    if(toolItem){
      toolItem.selected = !toolItem.selected;
    }
}
  1. 公共组件
    SvgIcon.vue
javascript 复制代码
<template>
  <i v-html="svgContent" :style="{ width:width, height:height }"></i>
</template>

<script setup lang="ts">
    import { onMounted, ref } from 'vue';

    interface PropData{
        src: string,
        width: string|number,
        height: string|number
    }

    const {src='',width='12px',height='12px'} = defineProps<PropData>();

    const svgContent = ref("");

    const baseUrl = new URL('@/assets/icons/', import.meta.url).href

    onMounted(async ()=>{
        const response = await fetch(baseUrl+'/'+src);
        const svgText = await response.text();
        // 转换成 Element Plus 风格
        const updatedSvgText = svgText
          .replace(/fill="[^"]*"/g, 'fill="currentColor"')
          .replace(/(width|height)="[^"]*"/g, (match) => {
            if (match.startsWith('width')) {
              return `width="${width}"`;
            }
            return match.startsWith('height') ? `height="${height}"` : match;
          });
 
        svgContent.value = updatedSvgText;
    })
</script>

<style scoped>
svg {
  display: inline-block;
  vertical-align: middle;
}
</style>

4.2 系统配置

javascript 复制代码
<template>
  <el-dialog class="config-dialog" 
    v-model="dialogVisible" title="系统配置" width="60vw" draggable center destroy-on-close :transition="transitionConfig" align-center append-to-body
    @closed="emits('closed')">
    <el-scrollbar height="50vh">
         <el-form ref="configFormRef" :model="configForm" :rules="rules"  label-width="auto" class="config-form">
            <el-divider content-position="left">基础配置</el-divider>
            <div class="basic-form">
                <el-row>
                    <el-col :span="8">
                        <el-form-item label="顶部工具栏自动隐藏" prop="topBarVisabled">
                            <el-switch v-model="configForm.topBarVisabled" @change="topBarVisabledChange" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="当前的课程" prop="currentTop">
                            <el-select v-model="configForm.currentTop" placeholder="请选择当前的课程"  @change="currentTopChange">
                                <el-option
                                    v-for="item in topTools"
                                    :key="item.key"
                                    :label="item.name"
                                    :value="item.key"
                                />
                            </el-select>
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="最大支出" prop="d3Config.maxSize">
                             <el-input-number v-model="configForm.d3Config.maxSize" :step="1" @change="maxSizeChange" />
                        </el-form-item>
                    </el-col>
                </el-row>
            </div>

            <el-divider content-position="left">手写参数</el-divider>
            <div class="basic-form">
                <el-row>
                    <el-col :span="8">
                        <el-form-item label="背景颜色" prop="d3Config.handWriting.planeColor">
                            <el-color-picker color-format="hex" v-model="configForm.d3Config.handWriting.planeColor" @active-change="planeColorChange" @change="planeColorChange"/>
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="背景纹理" prop="d3Config.handWriting.planeTexture">
                            <el-select v-model="configForm.d3Config.mesh.fill.type" placeholder="请选择纹理"  @change="planeTextureChange">
                                 <el-option v-for="item in textures" :key="item.value" :label="item.label" :value="item.value" />
                            </el-select>
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="背景透明度" prop="d3Config.handWriting.planeOpacity">
                            <el-slider v-model="configForm.d3Config.handWriting.planeOpacity" step="0.1" :min="0" :max="1"
                                @input="planeOpacityChange" @change="planeOpacityChange"/>
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="线条的颜色" prop="d3Config.handWriting.line.color">
                            <el-color-picker color-format="hex" v-model="configForm.d3Config.handWriting.line.color" @active-change="lineColorChange" @change="lineColorChange"/>
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="线条的尺寸" prop="d3Config.handWriting.line.width">
                             <el-input-number v-model="configForm.d3Config.handWriting.line.width" :step="1" @change="lineWidthChange" />
                        </el-form-item>
                    </el-col>
                </el-row>
            </div>

            <el-divider content-position="left">板擦参数</el-divider>
            <div class="basic-form">
                <el-row>
                    <el-col :span="8">
                        <el-form-item label="颜色" prop="d3Config.eraser.color">
                            <el-color-picker color-format="hex" v-model="configForm.d3Config.eraser.color" @active-change="eraserColorChange" @change="eraserColorChange"/>
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="半径" prop="d3Config.eraser.radius">
                            <el-slider v-model="configForm.d3Config.eraser.radius" step="0.001" :min="configForm.d3Config.camera.near" :max="eraserMaxRadius"
                                @input="eraserRadiusChange" @change="eraserRadiusChange"/>
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                    </el-col>
                </el-row>
            </div>

            <el-divider content-position="left">图形参数</el-divider>
            <div class="basic-form">
                <el-row>
                    <el-col :span="8">
                        <el-form-item label="背景颜色" prop="d3Config.mesh.d2PlaneColor">
                            <el-color-picker color-format="hex" v-model="configForm.d3Config.mesh.d2PlaneColor" @active-change="d2PlaneColorChange" @change="d2PlaneColorChange"/>
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="背景纹理" prop="d3Config.mesh.d2PlaneTexture">
                            <el-select v-model="configForm.d3Config.mesh.d2PlaneTexture" placeholder="请选择纹理"  @change="d2PlaneTextureChange">
                                 <el-option v-for="item in textures" :key="item.value" :label="item.label" :value="item.value" />
                            </el-select>
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="背景透明度" prop="d3Config.mesh.d2PlaneOpacity">
                            <el-slider v-model="configForm.d3Config.mesh.d2PlaneOpacity" step="0.1" :min="0" :max="1"
                                @input="d2PlaneOpacityChange" @change="d2PlaneOpacityChange"/>
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="线条的颜色" prop="d3Config.mesh.line.color">
                            <el-color-picker color-format="hex" v-model="configForm.d3Config.mesh.line.color" @active-change="meshLineColorChange" @change="meshLineColorChange"/>
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="线条的尺寸" prop="d3Config.mesh.line.width">
                             <el-input-number v-model="configForm.d3Config.mesh.line.width" :step="1" @change="meshLineWidthChange" />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                    <el-col :span="8">
                        <el-form-item label="填充类型" prop="d3Config.mesh.fill.type">
                            <el-select v-model="configForm.d3Config.mesh.fill.type" placeholder="请选择纹理类型"  @change="meshFillTypeChange">
                                <el-option label="纯色" :value="FillType.Color"/>
                                <el-option label="纹理" :value="FillType.Texture"/>
                                <el-option label="渐变色" :value="FillType.GradientColor"/>
                                <el-option label="自定义" :value="FillType.Customer"/>
                            </el-select>
                        </el-form-item>
                    </el-col>

                    <template v-if="configForm.d3Config.mesh.fill.type === FillType.Color">
                        <el-col :span="8">
                            <el-form-item label="填充颜色" prop="d3Config.mesh.fill.color">
                                <el-color-picker color-format="hex" v-model="configForm.d3Config.mesh.fill.color" @active-change="meshFillColorChange" @change="meshFillColorChange"/>
                            </el-form-item>
                        </el-col>
                        <el-col :span="8">
                        </el-col>
                    </template>
                    <template v-if="configForm.d3Config.mesh.fill.type === FillType.Texture">
                        <el-col :span="16">
                            <el-form-item label="纹理" prop="d3Config.handWriting.line.width">
                                
                            </el-form-item>
                        </el-col>
                    </template>
                    <template v-if="configForm.d3Config.mesh.fill.type === FillType.GradientColor">
                        <el-col :span="16">
                            <el-form-item label="纹理" prop="d3Config.handWriting.line.width">
                                
                            </el-form-item>
                        </el-col>
                    </template>
                    <template v-if="configForm.d3Config.mesh.fill.type === FillType.Customer">
                        <el-col :span="16">
                            <el-form-item label="纹理" prop="d3Config.handWriting.line.width">
                                
                            </el-form-item>
                        </el-col>
                    </template>
                </el-row>
            </div>

            <el-divider content-position="left">灯光配置</el-divider>
            <div class="basic-form">
                <el-row>
                    <el-col :span="8">
                        <el-form-item label="颜色" prop="d3Config.light.color">
                            <el-color-picker color-format="hex" v-model="configForm.d3Config.light.color" @active-change="lightColorChange" @change="lightColorChange"/>
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="亮度" prop="d3Config.light.intensity">
                            <el-slider v-model="configForm.d3Config.light.intensity" step="0.1" :min="0" :max="1"
                                @input="lightIntensityChange" @change="lightIntensityChange"/>
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                    </el-col>
                </el-row>
            </div>

            <el-divider content-position="left">场景配置</el-divider>
            <div class="basic-form">
                <el-row>
                    <el-col :span="8">
                        <el-form-item label="背景颜色" prop="d3Config.scene.background">
                            <el-color-picker color-format="hex" v-model="configForm.d3Config.scene.background" @active-change="backgroundChange" @change="backgroundChange"/>
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="纹理" prop="d3Config.scene.texture">
                            <el-select v-model="configForm.d3Config.scene.background" :placeholder="'请选择纹理信息'"  @change="sceneTextureChange">
                                <el-option
                                    v-for="item in textures"
                                    :key="item.value"
                                    :label="item.label"
                                    :value="item.value"
                                >
                                    <el-image v-if="item.value" :src="item.value" style="width:100%;height:30px;margin-bottom: 5px;"></el-image>
                                    <el-text v-else>无纹理</el-text>
                                </el-option>
                            </el-select>
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="环境纹理" prop="d3Config.scene.environment">
                            <el-select v-model="configForm.d3Config.scene.environment" :placeholder="'请选择纹理信息'"  @change="sceneEnvironmentChange">
                                <el-option
                                    v-for="item in textures"
                                    :key="item.value"
                                    :label="item.label"
                                    :value="item.value"
                                >
                                    <el-image v-if="item.value" :src="item.value" style="width:100%;height:30px;margin-bottom: 5px;"></el-image>
                                    <el-text v-else>无纹理</el-text>
                                </el-option>
                            </el-select>
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                    <el-col :span="8">
                        <el-form-item label="背景模糊度" prop="d3Config.scene.backgroundBlurriness">
                            <el-slider v-model="configForm.d3Config.scene.backgroundBlurriness" step="0.1" :min="0" :max="1"
                                @input="sceneBackgroundBlurrinessChange" @change="sceneBackgroundBlurrinessChange"/>
                        </el-form-item>
                    </el-col>
                </el-row>
            </div>

            <el-divider content-position="left">摄像机的配置</el-divider>
            <div class="basic-form">
                <el-row>
                    <el-col :span="8">
                        <el-form-item label="摄像机的视角" prop="d3Config.camera.fov">
                             <el-input-number v-model="configForm.d3Config.camera.fov" :step="1" :max="360" @change="handleFovChange" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="摄像机的近端面" prop="d3Config.camera.near">
                             <el-input-number v-model="configForm.d3Config.camera.near" :step="0.001" @change="handleNearChange" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="摄像机的远端面" prop="d3Config.camera.far">
                             <el-input-number v-model="configForm.d3Config.camera.far" :step="0.001" @change="handleFarChange" />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                    <el-col :span="8">
                        <el-form-item label="摄像机的缩放倍数" prop="d3Config.camera.zoom">
                             <el-input-number v-model="configForm.d3Config.camera.zoom" :min="0" :step="0.1" @change="handleZoomChange" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="16">
                        <el-form-item label="摄像机的位置" prop="d3Config.camera.position" class="">
                             <el-row :gutter="20">
                                <el-col :span="8">
                                    <el-input-number v-model="configForm.d3Config.camera.position.x" :step="0.001" @change="handleCameraPostionXChange" />
                                </el-col>
                                <el-col :span="8">
                                    <el-input-number v-model="configForm.d3Config.camera.position.y" :step="0.001" @change="handleCameraPostionYChange" />
                                </el-col>
                                <el-col :span="8">
                                    <el-input-number v-model="configForm.d3Config.camera.position.z" :step="0.001" @change="handleCameraPostionZChange" />
                                </el-col>
                             </el-row>
                        </el-form-item>
                    </el-col>
                </el-row>
            </div>
         </el-form>
    </el-scrollbar>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="dialogVisible = false">
          关闭
        </el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup lang="ts">
import { computed, ref,reactive } from "vue";
import type { DialogTransition } from 'element-plus'
import * as THREE from "three"
import  D3 from "@/views/d3"

import type { FormInstance, FormRules } from 'element-plus'
import { useConfigStore } from "@/stores/config";
import { useStateStore } from "@/stores/state";
import { useToolStore } from "@/stores/tool";
import { FillType, type D3Config } from "@/model/D3Config";
import { useTextureStore } from '@/stores/textures';

  //内置的纹理选项
  const textures = useTextureStore().textures;

//课程切换选项
const topTools = computed(()=>useToolStore().tools.top);

//自定义事件
const emits = defineEmits(['closed'])

//系统配置弹出框
const dialogVisible = ref(true);

//弹出框的动画效果
const transitionConfig = computed<DialogTransition>(() => {
  return {
      name: 'dialog-custom-object',
      appear: true,
      mode: 'out-in',
      duration: 500,
    }
})
interface ConfigForm {
    topBarVisabled:boolean,
    currentTop:string,
    d3Config:D3Config
}
const configFormRef = ref<FormInstance>()
const configForm = reactive<ConfigForm>({
  topBarVisabled:useConfigStore().topBarVisabled,
  currentTop:useStateStore().currentTop,
  d3Config:{
    maxSize:useConfigStore().d3Config.maxSize,
    camera:{
        fov:useConfigStore().d3Config.camera.fov,
        near:useConfigStore().d3Config.camera.near,
        far:useConfigStore().d3Config.camera.far,
        zoom:useConfigStore().d3Config.camera.zoom,
        position:useConfigStore().d3Config.camera.position
    },
    scene:{
        background:useConfigStore().d3Config.scene.background,
        texture:useConfigStore().d3Config.scene.texture,
        environment:useConfigStore().d3Config.scene.environment,
        backgroundBlurriness:useConfigStore().d3Config.scene.backgroundBlurriness
    },
    light:{
        intensity:useConfigStore().d3Config.light.intensity,
        color:useConfigStore().d3Config.light.color
    },
    handWriting:{
        planeColor:useConfigStore().d3Config.handWriting.planeColor.getHexString(),
        planeOpacity:useConfigStore().d3Config.handWriting.planeOpacity,
        planeTexture:useConfigStore().d3Config.handWriting.planeTexture,
        line:{
            color:useConfigStore().d3Config.handWriting.line.color.getHexString(),
            width:useConfigStore().d3Config.handWriting.line.width
        }
    },
    eraser:{
        color:useConfigStore().d3Config.eraser.color,
        radius:useConfigStore().d3Config.eraser.radius
    },
    mesh:{
        //二维时候的背景板的颜色
        d2PlaneColor:useConfigStore().d3Config.mesh.d2PlaneColor.getHexString(),
        d2PlaneOpacity:useConfigStore().d3Config.mesh.d2PlaneOpacity,
        d2PlaneTexture:useConfigStore().d3Config.mesh.d2PlaneTexture,
        //物体的线条颜色
        line:{
            width:useConfigStore().d3Config.mesh.line.width,
            color:useConfigStore().d3Config.mesh.line.color.getHexString()
        },
        //物体的填充颜色
        fill:{
            type:useConfigStore().d3Config.mesh.fill.type,
            color:useConfigStore().d3Config.mesh.fill.color.getHexString(),
            texture:useConfigStore().d3Config.mesh.fill.texture,
            customer:useConfigStore().d3Config.mesh.fill.customer
        }
    }
  }
})
const rules = reactive<FormRules<ConfigForm>>({
    [configForm.d3Config.scene.background]: [
        { required: true, message: '请选择背景颜色', trigger: 'blur' },
    ],
});

//橡皮擦的最大值
const eraserMaxRadius = computed(()=>{
    if(!D3.camera){
        return useConfigStore().d3Config.camera.far;
    }
    let minSize = D3.camera.getFilmHeight();
    if(minSize>D3.camera.getFilmWidth()){
        minSize = D3.camera.getFilmWidth();
    }
    return minSize/2;
})

//摄像机的视野
const handleFovChange = (value:number)=>{
    useConfigStore().d3Config.camera.fov = value;
}
//摄像机的近断面
const handleNearChange = (value:number)=>{
    useConfigStore().d3Config.camera.near = value;
}
//摄像机的远端面
const handleFarChange = (value:number)=>{
    useConfigStore().d3Config.camera.far = value;
}
//摄像机的缩放信息
const handleZoomChange = (value:number)=>{
    useConfigStore().d3Config.camera.zoom = value;
}
//摄像机的位置X变动
const handleCameraPostionXChange = (value:number)=>{
    useConfigStore().d3Config.camera.position.x = value;
}
//摄像机的位置Y变动
const handleCameraPostionYChange = (value:number)=>{
    useConfigStore().d3Config.camera.position.y = value;
}
//摄像机的位置Z变动
const handleCameraPostionZChange = (value:number)=>{
    useConfigStore().d3Config.camera.position.z = value;
}
//顶部工具栏自动隐藏状态变动
const topBarVisabledChange = (value:boolean)=>{
    useConfigStore().topBarVisabled = value;
}
//当前课程变动事件
const currentTopChange = (value:string)=>{
    useStateStore().currentTop = value;
}
//最大尺寸的变动
const maxSizeChange = (value:number)=>{
    useConfigStore().d3Config.maxSize = value;
}
//手写背景颜色的变动
const planeColorChange = (value:string)=>{
    useConfigStore().d3Config.handWriting.planeColor = new THREE.Color(value);
}
//背景板纹理的变动
const planeTextureChange = (value:THREE.Texture)=>{
    useConfigStore().d3Config.handWriting.planeTexture = value;
}
//背景透明度的变动
const planeOpacityChange = (value:number)=>{
    useConfigStore().d3Config.handWriting.planeOpacity = value;
}
//手写线条的颜色
const lineColorChange = (value:string)=>{
    useConfigStore().d3Config.handWriting.line.color = new THREE.Color(value);
}
//手写线条的尺寸
const lineWidthChange = (value:number)=>{
    useConfigStore().d3Config.handWriting.line.width = value;
}

//板擦的颜色变动
const eraserColorChange = (value:string)=>{
    useConfigStore().d3Config.eraser.color = new THREE.Color(value);
}
//板擦的半径变动
const eraserRadiusChange = (value:number)=>{
    useConfigStore().d3Config.eraser.radius = value;
}

//二维或三五物体的配置
const d2PlaneColorChange = (value:string)=>{
    useConfigStore().d3Config.mesh.d2PlaneColor = new THREE.Color(value);
}
//二维或三五物体背景板纹理的变动
const d2PlaneTextureChange = (value:THREE.Texture)=>{
    useConfigStore().d3Config.mesh.d2PlaneTexture = value;
}
//二维或三五物体背景透明度的变动
const d2PlaneOpacityChange = (value:number)=>{
    useConfigStore().d3Config.mesh.d2PlaneOpacity = value;
}
const meshLineColorChange = (value:string)=>{
    useConfigStore().d3Config.mesh.line.color = new THREE.Color(value);
}
const meshLineWidthChange = (value:number)=>{
    useConfigStore().d3Config.mesh.line.width = value;
}
const meshFillTypeChange = (value:FillType)=>{
    useConfigStore().d3Config.mesh.fill.type = value;
}
const meshFillColorChange = (value:string)=>{
    useConfigStore().d3Config.mesh.fill.color = new THREE.Color(value);
}

//场景
//背景颜色变动
const backgroundChange = (value:string)=>{
    useConfigStore().d3Config.scene.background = value;
}
const sceneBackgroundBlurrinessChange = (value:number)=>{
    useConfigStore().d3Config.scene.backgroundBlurriness = value;
}
const sceneTextureChange = (value:string)=>{
    useConfigStore().d3Config.scene.texture = value;
}
const sceneEnvironmentChange = (value:string)=>{
    useConfigStore().d3Config.scene.environment = value;
}

//灯光
const lightColorChange = (value:string)=>{
    useConfigStore().d3Config.light.color =new THREE.Color(value);
}
const lightIntensityChange = (value:number)=>{
    useConfigStore().d3Config.light.intensity = value;
}

</script>
<style lang="less" scoped>
    // .config-dialog{
        // --el-dialog-bg-color:#000000;
        // --el-text-color-primary:#ffffff
    // }
    .config-form{
       width: 100%; 

       .el-select{
            width:100%;
       }
    }
</style>

4.3 工具栏按钮

  1. bottom.vue
javascript 复制代码
<template>
  <div class="bottom-btns">
    <div class="inner">
      <el-button-group>
        <el-tooltip
          v-for="(item, index) in tools"
          v-if="!loading"
          effect="dark"
          :content="item.name"
          placement="top"
        >
          <el-button
            :round="index == 0 || index === tools.length - 1"
            :class="[toolItemSelected(item.key) ? 'selected' : '']"
            type="success" 
            :disabled="((useStateStore().currentMode != OprationModes.D3 && (item.key == 'AxesHelper' || item.key == 'CameraHelper' || item.key == 'OrbitControls')) || (useStateStore().currentMode != OprationModes.HandWriting && item.key == 'eraser'))"
            @click="commond(item)"
          >
            <SvgIcon :src="item.icon" width="20px" height="20px" style="color:#fff"></SvgIcon>
          </el-button>
        </el-tooltip>
      </el-button-group>
    </div>
  </div>
  <!-- 系统配置弹出框 -->
  <ConfigDilaog
    ref="configRef"
    v-if="configDialog"
    @closed="configDialog = false"
  ></ConfigDilaog>
</template>
<script setup lang="ts">
import { useToolStore } from "@/stores/tool";
import { commond } from "@/utils/CommondHandle";
import ConfigDilaog from "../dialog/ConfigDilaog.vue";
import {  onBeforeUnmount, onMounted, ref } from "vue";
import EventBus from "@/utils/EventBus";
import {
  COMMOND_TYPE_D2_MODEL,
  COMMOND_TYPE_D3_HELP,
  COMMOND_TYPE_D3_MODEL,
  COMMOND_TYPE_D3_OP,
  COMMOND_TYPE_OPEN_CONFIG_DIALOG,
  COMMOND_TYPE_SELF_WRITE,
  COMMOND_TYPE_SELF_WRITE_STATUS_CHANGE,
} from "@/common/Constant";
import type { ToolBarItem } from "@/model/ToolItem/ToolBartem";
import { changeToolItemSelected } from "@/utils/Utils";
import  D3 from "../../d3";
import { ElMessage } from "element-plus";
import { useStateStore } from "@/stores/state";
import { OprationModes } from "@/model/OprationModes";

//全局状态的监听
const state = useStateStore();
//当前的工具按钮
const tools = useToolStore().tools.bottom;

//配置项的弹框是否展示
const configDialog = ref(false);

//当前工具按钮是否被选中
const toolItemSelected = (item_: string) => {
  if (!item_) {
    return false;
  }
  let toolItem = tools.find((item:ToolBarItem) => item.key === item_);
  return toolItem && toolItem.selected;
};

//是否正在同步数据
const loading = ref(true);

onMounted(() => {
  loading.value = false;

  //订阅事件
  EventBus.on(COMMOND_TYPE_OPEN_CONFIG_DIALOG, (item) => {
    configDialog.value = true;
  });
  EventBus.on(COMMOND_TYPE_SELF_WRITE, (item_) => {
    changeToolItemSelected(
      tools,
      item_ as ToolBarItem,
      COMMOND_TYPE_SELF_WRITE
    );
    if ((item_ as ToolBarItem).selected) {
      state.currentMode = OprationModes.HandWriting;
      D3.stopMesh2D();
      D3.stopMesh3D();
      D3.starThandWriting();
    } else {
      state.currentMode = OprationModes.D3;
      D3.stopThandWriting();
    }
  });
  EventBus.on(COMMOND_TYPE_D2_MODEL, (item_) => {
    changeToolItemSelected(
      tools,
      item_ as ToolBarItem,
      COMMOND_TYPE_D2_MODEL
    );
    if ((item_ as ToolBarItem).selected) {
      state.currentMode = OprationModes.D2;
      D3.stopThandWriting();
      D3.stopMesh3D();
      D3.startMesh2D(null);
    } else {
      state.currentMode = OprationModes.D3;
      D3.stopMesh2D();
    }
  });
  EventBus.on(COMMOND_TYPE_D3_MODEL, (item_) => {
    changeToolItemSelected(
      tools,
      item_ as ToolBarItem,
      COMMOND_TYPE_D3_MODEL
    );
    if ((item_ as ToolBarItem).selected) {
      state.currentMode = OprationModes.D3;
      D3.stopThandWriting();
      D3.stopMesh2D();
      D3.startMesh3D(null);
    } else {
      state.currentMode = OprationModes.D3;
      D3.stopMesh3D();
    }
  });
  EventBus.on(COMMOND_TYPE_D3_HELP, (item_) => {
    changeToolItemSelected(tools, item_ as ToolBarItem, COMMOND_TYPE_D3_HELP);
    switch ((item_ as ToolBarItem).key) {
      case "OrbitControls": //轨道控制器(OrbitControls)
        D3.setControlEnabled((item_ as ToolBarItem).selected ? true : false);
        break;
      case "CameraHelper": //模拟相机视锥体的辅助对象
        D3.setCameraHelper((item_ as ToolBarItem).selected ? true : false);
        break;
      case "AxesHelper": //坐标轴
        D3.setAxesHelper((item_ as ToolBarItem).selected ? true : false);
        break;
      default:
        break;
    }
  });
  EventBus.on(COMMOND_TYPE_D3_OP, (item_) => {
    switch ((item_ as ToolBarItem).key) {
      case "clear": //清空
        D3.clear();
        break;
      case "add": //放大
        D3.add();
        break;
      case "sub": //缩小
        D3.sun();
        break;
      case "reset": //复位
        D3.reset();
        break;
      case "eraser": //橡皮擦
        if (!(item_ as ToolBarItem).selected) {
          let handWritingItem = useToolStore().getItemByKeyAndCommond(
            "write",
            COMMOND_TYPE_SELF_WRITE
          );
          if (!handWritingItem || !handWritingItem.selected) {
            ElMessage.warning("请先开启手写模式~");
            return;
          }
        }
        changeToolItemSelected(tools, item_ as ToolBarItem, COMMOND_TYPE_D3_OP);
        if ((item_ as ToolBarItem).selected) {
          D3.startEraser();
        } else {
          D3.stopEraser();
        }
        break;
      default:
        break;
    }
  });
  //手写状态变动的事件
  EventBus.on(COMMOND_TYPE_SELF_WRITE_STATUS_CHANGE, (item_) => {
    useToolStore().tools.bottom.forEach((item:ToolBarItem) => {
      if (item.status?.includes(COMMOND_TYPE_SELF_WRITE)) {
        //关联了手写状态
        item.selected = item_ ? true : false;

        switch ((item as ToolBarItem).key) {
          case "OrbitControls": //轨道控制器(OrbitControls)
            D3.setControlEnabled(item.selected ? true : false);
            if(state.currentMode == OprationModes.D3){
              D3.setControlEnabled(false);
              item.selected = false;
            }
            break;
          case "CameraHelper": //模拟相机视锥体的辅助对象
            D3.setCameraHelper(item.selected ? true : false);
            break;
          case "AxesHelper": //坐标轴
            D3.setAxesHelper(item.selected ? true : false);
            break;
          default:
            break;
        }
      }
    });
  });
});
onBeforeUnmount(() => {
  EventBus.off(COMMOND_TYPE_OPEN_CONFIG_DIALOG);
  EventBus.off(COMMOND_TYPE_SELF_WRITE);
  EventBus.off(COMMOND_TYPE_D3_HELP);
  EventBus.off(COMMOND_TYPE_D3_OP);
  EventBus.off(COMMOND_TYPE_SELF_WRITE_STATUS_CHANGE);
  EventBus.off(COMMOND_TYPE_D2_MODEL);
  EventBus.off(COMMOND_TYPE_D3_MODEL);
});
</script>
<style lang="less" scoped>
.bottom-btns {
  position: fixed;
  right: 0px;
  bottom: 10px;
  height: 60px;
  width: 100%;
  display: flex;
  justify-items: center;
  justify-content: center;
  align-items: center;
  align-content: center;

  .inner {
    min-width: 300px;

    .el-button-group {
      width: 100%;
    }

    .el-button {
      --el-button-bg-color: #67c23a33;
      --el-button-border-color: #67c23a33;
      flex: 1;
    }

    .selected {
      --el-button-bg-color: var(--el-color-success);
    }
  }
}
</style>
  1. free.vue
javascript 复制代码
<template>
    <div class="tool-btns">
      <div class="inner">
          <!-- <el-button type="danger" circle :icon="Setting"></el-button> -->
      </div>
    </div>
</template>
<script setup lang="ts">
    import {Setting} from '@element-plus/icons-vue'
</script>
<style lang="less" scoped>
    .tool-btns{
        position: fixed;
        right: 5%;
        bottom: 10%;
    }
</style>
  1. left.vue
javascript 复制代码
<template>
  <div class="left-btns">
    <Transition>
      <div class="inner" v-show="open">
        <el-tabs class="left-tabs" type="border-card" v-model="currentTab">
          <el-tab-pane :label="item.name" :name="item.name" v-for="item in tabs">
            <el-scrollbar height="calc(600px - 40px - 20px)">
              <template v-for="groupItem in item.contents">
                <el-divider content-position="left">{{groupItem.name}}</el-divider>
                <el-row>
                  <el-col :span="4" v-for="toolItem in groupItem.children" class="btn-item">
                    <el-tooltip
                      effect="dark"
                      :content="toolItem.name"
                      placement="top"
                    >
                      <el-button class="tool-btn"
                        :type="(toolItem as ToolBarMeshItem).selected ? 'danger' :'default'" @click="commond(toolItem)"
                        circle plain
                      >
                        <SvgIcon :src="toolItem.icon" width="15px" height="15px" ></SvgIcon>
                        <!-- {{ toolItem.name }} -->
                      </el-button>
                    </el-tooltip>
                  </el-col>
                </el-row>
              </template>
            </el-scrollbar>
          </el-tab-pane>
        </el-tabs>
      </div>
    </Transition>
    <div class="collspan-btns" :class="[open?'open':'close']">
      <el-button :type="open?'danger':'primary'" :icon="open?SemiSelect:Plus" circle @click="open = !open"/>
    </div>
  </div>
</template>
<script setup lang="ts">
import {
  COMMOND_TYPE_2D_MESH,
  COMMOND_TYPE_3D_MESH,
  COMMOND_TYPE_CHANGE_MODEL,
  COMMOND_TYPE_D2_MODEL,
  COMMOND_TYPE_D3_MODEL,
  COMMOND_TYPE_SELF_WRITE_STATUS_CHANGE,
} from "@/common/Constant";
import type { ToolBarItem } from "@/model/ToolItem/ToolBartem";
import { useStateStore } from "@/stores/state";
import { useToolStore } from "@/stores/tool";
import { commond } from "@/utils/CommondHandle";
import EventBus from "@/utils/EventBus";
import {  nextTick, onBeforeUnmount, onMounted, reactive, ref } from "vue";
import { Plus,SemiSelect } from "@element-plus/icons-vue";
import D3  from "../../d3";
import type { ToolBarMeshItem } from "@/model/ToolItem/ToolBarMeshItem";
import { OprationModes } from "@/model/OprationModes";
import type { TabItem } from "@/model/ToolItem/TabItem";

//tab标签
const tabs = reactive<TabItem[]>([
  {
    name: "二维",
    contents: [...useToolStore().tools.free.filter((item:ToolBarItem)=>item.fkey === '2d')],
  },
  {
    name: "三维",
    contents: [...useToolStore().tools.free.filter((item:ToolBarItem)=>item.fkey === '3d')],
  },
]);

//全局状态的监听
const state = useStateStore();

//是否开启工具栏
const open = ref(true);

//当前激活的tab页
const currentTab = ref("二维");

//工具栏
const toolStore = useToolStore();

/**
 * 切换顶部的分组
 * @param key 
 */
const switchTopTool = (key:String)=>{
  let name = useToolStore().tools.top.find((item:ToolBarItem)=>item.key === key);
  if(!name){
    return;
  }
  useToolStore().initItemByKey(key);
  let tools = useToolStore().tools.left.filter(
    (item:ToolBarItem) => item.fkey === key
  );
  useToolStore().tools.top.forEach((item:ToolBarItem)=>{
    let index = tabs.findIndex(itme=>itme.name === item.name);
    if(index>0){
      tabs.splice(index,1);
    }
  })
  if(tabs.find(itme=>itme.name === name.name)){
    let index = tabs.findIndex(itme=>itme.name === name.name);
    tabs.splice(index,1);
  }else{
    tabs.push({
      name:name?.name as string,
      contents:[...tools]
    });
  }
  currentTab.value = name?.name as string;
}

onMounted(() => {
  //订阅事件
  EventBus.on(COMMOND_TYPE_CHANGE_MODEL, (item) => {
    switchTopTool(useStateStore().currentTop)
  });
  EventBus.on(COMMOND_TYPE_SELF_WRITE_STATUS_CHANGE, (item) => {
    let item_ = toolStore.getItemByKey("write");
    if(item_ && item_.selected){
      open.value = false;
    }else{
      open.value = true;
      // EventBus.emit(COMMOND_TYPE_SELF_WRITE, toolStore.getItemByKey("write"));
      // EventBus.emit(COMMOND_TYPE_D3_OP, toolStore.getItemByKey("eraser"));
    }
  });
  EventBus.on(COMMOND_TYPE_D2_MODEL, (item) => {
    let item_ = toolStore.getItemByKey("d2");
    if(item_ && item_.selected){
      open.value = true;
      currentTab.value = '二维';
      useStateStore().cursor = "default";
      // EventBus.emit(COMMOND_TYPE_SELF_WRITE, toolStore.getItemByKey("write"));
      // EventBus.emit(COMMOND_TYPE_D3_OP, toolStore.getItemByKey("eraser"));
    }
  });
  EventBus.on(COMMOND_TYPE_D3_MODEL, (item) => {
    let item_ = toolStore.getItemByKey("d3");
    if(item_ && item_.selected){
      open.value = true;
      currentTab.value = '三维';
      useStateStore().cursor = "default";
      // EventBus.emit(COMMOND_TYPE_SELF_WRITE, toolStore.getItemByKey("write"));
      // EventBus.emit(COMMOND_TYPE_D3_OP, toolStore.getItemByKey("eraser"));
    }
  });
  EventBus.on(COMMOND_TYPE_2D_MESH,(item)=>{
    if((item as ToolBarMeshItem).selected){//已选中,点击之后变未选中的状态
      D3.pauseMesh2D();
    }else{
      tabs.forEach(item=>{//将其他的选中状态清除
        if(item.contents){
          item.contents.forEach((item_:ToolBarItem)=>{
            item_.selected = false;
            if(item_.children){
              item_.children.forEach((item__:ToolBarItem)=>{
                item__.selected = false;
              });
            }
          })
        }
      })
      D3.stopThandWriting();
      D3.stopMesh3D();
      D3.startMesh2D(item as ToolBarMeshItem);
      state.currentMode = OprationModes.D2;
    }
    (item as ToolBarMeshItem).selected = !(item as ToolBarMeshItem).selected;
    open.value = true;
  });
  EventBus.on(COMMOND_TYPE_3D_MESH,(item)=>{
    if((item as ToolBarMeshItem).selected){//已选中,点击之后变未选中的状态
      D3.pauseMesh3D();
    }else{
      tabs.forEach(item=>{//将其他的选中状态清除
        if(item.contents){
          item.contents.forEach((item_:ToolBarItem)=>{
            item_.selected = false;
            if(item_.children){
              item_.children.forEach((item__:ToolBarItem)=>{
                item__.selected = false;
              });
            }
          })
        }
      })
      D3.stopThandWriting();
      D3.stopMesh2D();
      D3.startMesh3D(item as ToolBarMeshItem);
      state.currentMode = OprationModes.D3;
    }
    (item as ToolBarMeshItem).selected = !(item as ToolBarMeshItem).selected;
    open.value = true;
  });

  //给一个默认的toptab
  nextTick(()=>{
    switchTopTool(useStateStore().currentTop)
  })
});

onBeforeUnmount(() => {
  EventBus.off(COMMOND_TYPE_CHANGE_MODEL);
  EventBus.off(COMMOND_TYPE_SELF_WRITE_STATUS_CHANGE);
  EventBus.off(COMMOND_TYPE_2D_MESH);
  EventBus.off(COMMOND_TYPE_3D_MESH);
  EventBus.off(COMMOND_TYPE_D2_MODEL);
  EventBus.off(COMMOND_TYPE_D3_MODEL);
});
</script>
<style lang="less" scoped>
  .v-enter-active,
  .v-leave-active {
    transition: opacity 0.5s ease;
  }

  .v-enter-from,
  .v-leave-to {
    opacity: 0;
  }
  
.left-btns {
  position: fixed;
  left: 20px;
  top: 0;
  height: 100%;
  width: 300px;
  display: flex;
  justify-items: center;
  justify-content: center;
  align-items: center;
  align-content: center;

    .collspan-btns{
      position: absolute;
      top:0;
      right:-30px;
      width:60px;
      height: 100%;
      display: flex;
      justify-items: center;
      justify-content: center;
      align-items: center;
      align-content: center;
    }
    .open{
      left:calc(100% - 30px)
    }
    .close{
      left:30px;
    }

  .inner {
    height: 600px;
    width: 300px;
    z-index: 1;

    .left-tabs {
      height: calc(600px - 60px);
      border-radius: var(--el-card-border-radius);

      --el-bg-color-overlay: #e4e7ed1c;
      --el-border-color: var(--tool-panel-border-color);
      --el-fill-color-light: #e4e7ed1c;
      --el-border-color-light: var(--el-border-color);
      --el-color-primary: #00ff2b;
      --el-card-border-radius: 10px;

      background: var(--tool-panel-bg-color);

      :deep(.el-tabs__header){
        border-top-left-radius: var(--el-card-border-radius);
        border-top-right-radius: var(--el-card-border-radius);
      }

      :deep(.el-tabs__item, .el-tabs__header) {
        border-top-left-radius: var(--el-card-border-radius);
        border-top-right-radius: var(--el-card-border-radius);
      }
      :deep(.el-tabs__content) {
        border-radius: var(--el-card-border-radius);
        padding: 5px;
        padding-top: 10px;

        .el-form-item__label {
          --el-text-color-regular: #fff;
        }
      }
      :deep(.el-divider__text){
        --el-bg-color: #ffffff00;
        --el-text-color-primary:#fff;
      }

      .tool-btn{
        --el-button-hover-text-color:#00ff00;
        --el-button-hover-bg-color:#00ff0044;
        --el-button-hover-border-color:#00ff0022;
        --el-button-text-color:#00ff00;
        --el-button-bg-color:#ffffff11;
        --el-button-border-color:#ffffff01;

      }

      :deep(.el-button--danger){
        --el-button-hover-text-color:#f0033e;
        --el-button-hover-bg-color:#ff000044;
        --el-button-hover-border-color:#ff000022;
        --el-button-text-color:#f0033e;
        --el-button-bg-color:#ff000030;
        --el-button-border-color:#ff000001;
      }

      .config-form {
        width: 90%;
      }
    }

    .btn-item{
      text-align: center;
      margin-bottom: 10px;
    }
  }
}
</style>
  1. right.vue
javascript 复制代码
<template>
  <div class="right-btns">
    <div class="collspan-btns" :class="[open ? 'open' : 'close']">
      <el-button :type="open ? 'danger' : 'primary'" :icon="open ? SemiSelect : Plus" circle @click="open = !open" />
    </div>
    <Transition>
      <div class="inner" v-show="open">
        <el-tabs type="border-card" class="tabs-panel" v-model="currentTab">
          <el-tab-pane v-for="item in tabs" :label="item.name" :name="item.name">
            <el-scrollbar height="calc(600px - 40px - 20px)">
              <el-form ref="configFormRef" :model="configForm" label-width="auto" class="config-form">
                <template v-for="contentItem in item.contents">
                  <template v-if="contentItem.type === 'divider'">
                    <el-divider content-position="center">{{
                      contentItem.name
                    }}</el-divider>
                  </template>
                  <el-form-item v-else :label="contentItem.name">
                    <template v-if="contentItem.type === 'slider'">
                      <el-slider v-model="configForm[contentItem.modelPath.join('_')]"
                        :step="contentItem.step ? contentItem.step : 1" :min="contentItem.min !== undefined
                            ? contentItem.min
                            : Number.MIN_SAFE_INTEGER
                          " :max="contentItem.max !== undefined
                            ? contentItem.max
                            : Number.MAX_SAFE_INTEGER
                          " @input="valueChange(contentItem, $event)" @change="valueChange(contentItem, $event)" />
                    </template>
                    <template v-if="contentItem.type === 'number'">
                      <el-input-number v-model="configForm[contentItem.modelPath.join('_')]"
                        :step="contentItem.step ? contentItem.step : 1" :min="contentItem.min !== undefined
                            ? contentItem.min
                            : Number.MIN_SAFE_INTEGER
                          " :max="contentItem.max !== undefined
                            ? contentItem.max
                            : Number.MAX_SAFE_INTEGER
                          " @change="valueChange(contentItem, $event)" />
                    </template>
                    <template v-else-if="contentItem.type === 'color'">
                      <el-row :gutter="10">
                        <el-col :span="19">
                          <el-input v-model="configForm[contentItem.modelPath.join('_')]
                            " readonly></el-input>
                        </el-col>
                        <el-col :span="4">
                          <el-color-picker color-format="hex" v-model="configForm[contentItem.modelPath.join('_')]
                            " @active-change="valueChange(contentItem, $event)"
                            @change="valueChange(contentItem, $event)" />
                        </el-col>
                      </el-row>
                    </template>
                    <template v-else-if="contentItem.type === 'select'">
                      <el-select v-model="configForm[contentItem.modelPath.join('_')]"
                        :placeholder="'请选择' + contentItem.name" @change="valueChange(contentItem, $event)">
                        <el-option v-for="item in contentItem.options" :key="item.value" :label="item.label"
                          :value="item.value" />
                      </el-select>
                    </template>
                    <template v-else-if="contentItem.type === 'texture'">
                      <el-select v-model="configForm[contentItem.modelPath.join('_')]"
                        :placeholder="'请选择' + contentItem.name" @change="valueChange(contentItem, $event)">
                        <el-option v-for="item in textures" :key="item.value" :label="item.label" :value="item.value">
                          <el-image v-if="item.value" :src="item.value" style="
                              width: 100%;
                              height: 30px;
                              margin-bottom: 5px;
                            "></el-image>
                          <el-text v-else>无纹理</el-text>
                        </el-option>
                      </el-select>
                    </template>
                  </el-form-item>
                </template>
              </el-form>
            </el-scrollbar>
          </el-tab-pane>
        </el-tabs>
      </div>
    </Transition>
  </div>
</template>
<script setup lang="ts">
import { useConfigStore } from "@/stores/config";
import { onMounted, reactive, ref, watch } from "vue";
import * as THREE from "three";
import { useStateStore } from "@/stores/state";
import EventBus from "@/utils/EventBus";
import {
  COMMOND_TYPE_2D_MESH,
  COMMOND_TYPE_3D_MESH,
  COMMOND_TYPE_D3_OP,
  COMMOND_TYPE_MESH_CANCEL_SELECTED,
  COMMOND_TYPE_MESH_SELECTED,
  COMMOND_TYPE_SELF_WRITE,
  FILL_TYPE,
} from "@/common/Constant";
import { onUnmounted } from "vue";
import { useToolStore } from "@/stores/tool";
import type { ToolBarItem } from "@/model/ToolItem/ToolBartem";
import { SemiSelect, Plus } from "@element-plus/icons-vue";
import { ToolBarMeshItemConfigItem, type ToolBarMeshItem } from "@/model/ToolItem/ToolBarMeshItem";
import TabItemContentItem from "@/model/ToolItem/TabItemContentItem";
import { useTextureStore } from "@/stores/textures";

//内置的纹理选项
const textures = useTextureStore().textures;

//全局配置信息
const configStore = useConfigStore();

//实时数据
const stateStore = useStateStore();

//工具栏
const toolStore = useToolStore();

interface TabItem {
  name: string;
  contents: TabItemContentItem[];
}

interface FormData {
  [key: string]: object | null | FormData | string|number;
}

//是否打开
const open = ref(true);

//标签页内容
const tabs = reactive<TabItem[]>([]);

//当前激活的tab
const currentTab = ref("对象属性");

//表单的内容
const configForm = reactive<FormData>({});

//当前的物体信息
const currentMeshItem = ref<ToolBarMeshItem>();

/**
 * 添加标签
 * @param item
 */
const addTab = (item: any, rootValue: FormData = configStore.d3Config) => {
  let tab = tabs.findIndex((item_) => item_.name === item.name);
  if (tab > 0) {
    currentTab.value = tabs[tabs.length - 1]?.name as string;
    return;
  }
  tabs.push(item);
  setValue(item?.contents as TabItemContentItem[], rootValue);
};
/**
 * 移除标签
 */
const removeTab = (name: String) => {
  let tab = tabs.findIndex((item) => item.name === name);
  if (tab > 0) {
    tabs.splice(tab, 1);
    currentTab.value = tabs[tabs.length - 1]?.name as string;
  }
};

/**
 * 值变动的事件
 * @param contentItem
 * @param event
 */
const valueChange = (contentItem: TabItemContentItem, event: Object) => {
  let value: any = configStore.d3Config;
  if (
    contentItem.auto &&
    currentMeshItem &&
    currentMeshItem.value &&
    (currentMeshItem.value as ToolBarMeshItem).config
  ) {
    value = (currentMeshItem.value as ToolBarMeshItem).config;
  }
  for (let i = 0; i < contentItem.modelPath.length - 1; i++) {
    if (value) {
      value = value[contentItem.modelPath[i] as string];
    }
  }
  // value[item.modelPath.join('_')] = value;
  // console.log(value,event);
  if (contentItem.type === "color") {
    value[contentItem.modelPath[contentItem.modelPath.length - 1] as string] =
      new THREE.Color(event);
  } else {
    value[contentItem.modelPath[contentItem.modelPath.length - 1] as string] =
      event;
  }
  
  if (contentItem.auto && stateStore.currentMesh && contentItem instanceof ToolBarMeshItemConfigItem && contentItem.updateConfig) {
    //物体自定义的配置
    contentItem.updateConfig(
      stateStore.currentMesh,
      value[contentItem.modelPath[contentItem.modelPath.length - 1] as string],
    );
  }
};

//设置初始化的数据
const setValue = (contents: TabItemContentItem[], rootValue: FormData) => {
  contents.forEach((item) => {
    let value: any = rootValue;
    for (let i = 0; i < item.modelPath.length; i++) {
      if (value) {
        value = value[item.modelPath[i] as string];
      }
    }
    if (item.type === "color" && value) {
      value = "#" + value.getHexString();
    }
    configForm[item.modelPath.join("_")] = value;
  });
};
//初始的
const init = () => {
  {
    //全局属性的配置
    let contents: TabItemContentItem[] = [];
    let tab = {
      name: "全局属性",
      contents: contents,
    };
    //获取三维配置,初始化tab属性栏
    contents.push(new TabItemContentItem("divider", "场景", []));
    contents.push(
      new TabItemContentItem("color", "背景色", ["scene", "background"]),
    );
    contents.push(
      new TabItemContentItem("texture", "纹理", ["scene", "texture"]),
    );
    contents.push(
      new TabItemContentItem("texture", "环境图", ["scene", "environment"]),
    );
    contents.push(
      new TabItemContentItem(
        "slider",
        "模糊度",
        ["scene", "backgroundBlurriness"],
        0,
        1,
        0.1,
      ),
    );
    contents.push(
      new TabItemContentItem(
        "number",
        "最大尺寸",
        ["maxSize"],
        0,
        configStore.d3Config.camera.far,
        0.1,
      ),
    );

    contents.push(new TabItemContentItem("divider", "灯光", []));
    contents.push(
      new TabItemContentItem("color", "环境光", ["light", "color"]),
    );
    contents.push(
      new TabItemContentItem(
        "slider",
        "光照强度",
        ["light", "intensity"],
        0,
        1,
        0.1,
      ),
    );

    contents.push(new TabItemContentItem("divider", "摄像机", []));
    contents.push(
      new TabItemContentItem("slider", "视角", ["camera", "fov"], 0, 360, 1),
    );
    contents.push(
      new TabItemContentItem(
        "number",
        "近端面",
        ["camera", "near"],
        undefined,
        undefined,
        configStore.d3Config.camera.near,
      ),
    );
    contents.push(
      new TabItemContentItem(
        "number",
        "远端面",
        ["camera", "far"],
        undefined,
        undefined,
        configStore.d3Config.camera.near,
      ),
    );
    contents.push(
      new TabItemContentItem(
        "number",
        "缩放倍数",
        ["camera", "zoom"],
        0,
        undefined,
        0.1,
      ),
    );
    contents.push(
      new TabItemContentItem(
        "number",
        "位置X",
        ["camera", "position", "x"],
        undefined,
        undefined,
        0.1,
      ),
    );
    contents.push(
      new TabItemContentItem(
        "number",
        "位置Y",
        ["camera", "position", "y"],
        undefined,
        undefined,
        0.1,
      ),
    );
    contents.push(
      new TabItemContentItem(
        "number",
        "位置Z",
        ["camera", "position", "z"],
        undefined,
        undefined,
        0.1,
      ),
    );

    addTab(tab);
  }

  //基础信息配置,主要绘制的物体相关属性
  {
    let contents: TabItemContentItem[] = [];
    let tab = {
      name: "对象属性",
      contents: contents,
    };
    contents.push(new TabItemContentItem("divider", "基础配置", []));
    contents.push(
      new TabItemContentItem("color", "背景板", ["mesh", "d2PlaneColor"]),
    );
    contents.push(
      new TabItemContentItem("slider", "背景板透明度", ["mesh", "d2PlaneOpacity"],0,1,0.1),
    );
    contents.push(
      new TabItemContentItem("texture", "背景纹理", ["mesh", "d2PlaneTexture"],undefined,undefined,undefined,textures),
    );
    contents.push(
      new TabItemContentItem("color", "线条颜色", ["mesh", "line", "color"]),
    );
    contents.push(
      new TabItemContentItem(
        "number",
        "线条尺寸",
        ["mesh", "line", "width"],
        0,
        undefined,
        configStore.d3Config.camera.near,
      ),
    );
    contents.push(
      new TabItemContentItem(
        "select",
        "填充类型",
        ["mesh", "fill", "type"],
        undefined,
        undefined,
        undefined,
        FILL_TYPE,
      ),
    );
    contents.push(
      new TabItemContentItem("color", "填充颜色", ["mesh", "fill", "color"]),
    );

    contents.push(new TabItemContentItem("divider", "物体属性", []));

    addTab(tab);
  }
};
/**
 * 初始化物体自定义的配置项
 * @param item
 */
const initAutoMeshConfig = (item: ToolBarMeshItem) => {
  //移除前一个对象的
  let objectFiledItems = tabs[1] as TabItem;
  objectFiledItems.contents.forEach((item) => {
    if (item.auto) {
      delete configForm[item.modelPath.join("_")];
    }
  });
  objectFiledItems.contents = objectFiledItems.contents.filter(
    (item_) => !item_.auto,
  );
  if (item && item.config) {
    for (const key in item.config) {
      let configItem: ToolBarMeshItemConfigItem<any> = item.config[key];
      if (!configItem.form) {
        continue;
      }
      objectFiledItems.contents.push(configItem);
      // objectFiledItems.contents.push(new TabItemContentItem(
      //   configItem.type,
      //   configItem.name,
      //   [...configItem.modelPath],
      //   configItem.min,
      //   configItem.max,
      //   configItem.step,
      //   configItem.options ? [...configItem.options] : [],
      //   true
      // ));
      configForm[configItem.modelPath.join("_")] = configItem.value;
      if (configItem.type === "color" && configItem.value) {
        configForm[configItem.modelPath.join("_")] =
          "#" + configItem.value.getHexString();
      }
    }
  }

  currentMeshItem.value = item as ToolBarMeshItem;
};

//监听当前视角位置变动
watch(
  () => configStore.d3Config.camera.position,
  (newVal, oldVal) => {
    configForm["camera_position_x"] = newVal.x;
    configForm["camera_position_y"] = newVal.y;
    configForm["camera_position_z"] = newVal.z;
  },
  {
    deep: true,
  },
);

onMounted(() => {
  init();
  //手写
  EventBus.on(COMMOND_TYPE_SELF_WRITE, (item_) => {
    let item = toolStore.getItemByKey("write");
    if (item && !item.selected) {
      let tab = {
        name: "手写板",
        contents: [
          new TabItemContentItem("color", "背景色", ["handWriting", "planeColor"]),
          new TabItemContentItem("slider", "背景板透明度", ["handWriting", "planeOpacity"],0,1,0.1),
          new TabItemContentItem("texture", "背景纹理", ["handWriting", "planeTexture"],undefined,undefined,undefined,textures),
          new TabItemContentItem("color", "线条色", ["handWriting", "line", "color"]),
          new TabItemContentItem("number", "线条大小", ["handWriting", "line", "width"], 0, undefined, configStore.d3Config.camera.near)
        ],
      };
      addTab(tab);
      currentTab.value = "手写板";
    } else {
      removeTab("手写板");
    }
  });
  //黑板擦
  EventBus.on(COMMOND_TYPE_D3_OP, (item_) => {
    switch ((item_ as ToolBarItem).key) {
      case "eraser": //橡皮擦
        {
          if (!(item_ as ToolBarItem).selected) {
            let handWritingItem = useToolStore().getItemByKeyAndCommond(
              "write",
              COMMOND_TYPE_SELF_WRITE,
            );
            if (!handWritingItem || !handWritingItem.selected) {
              removeTab("黑板擦");
              return;
            }
          }
          let item = toolStore.getItemByKey("eraser");
          if (item && !item.selected) {
            let tab = {
              name: "黑板擦",
              contents: [
                new TabItemContentItem("color", "背景色", ["eraser", "color"]),
                new TabItemContentItem("number", "线条大小", ["eraser", "radius"], 0, undefined, configStore.d3Config.camera.near),
              ],
            };
            addTab(tab);
            currentTab.value = "黑板擦";
          } else {
            removeTab("黑板擦");
          }
        }
        break;
      default:
        break;
    }
  });
  EventBus.on(COMMOND_TYPE_2D_MESH, (item) => {
    initAutoMeshConfig(item as ToolBarMeshItem);
  });
  EventBus.on(COMMOND_TYPE_3D_MESH, (item) => {
    initAutoMeshConfig(item as ToolBarMeshItem);
  });
  EventBus.on(COMMOND_TYPE_MESH_SELECTED, (item) => {
    initAutoMeshConfig(item as ToolBarMeshItem);
  });
  EventBus.on(COMMOND_TYPE_MESH_CANCEL_SELECTED, (item) => {
    let objectFiledItems = tabs[1] as TabItem;
    objectFiledItems.contents.forEach((item) => {
      if (item.auto) {
        delete configForm[item.modelPath.join("_")];
      }
    });
    objectFiledItems.contents = objectFiledItems.contents.filter(
      (item_) => !item_.auto,
    );
  });
});

//暴露方法
defineExpose({
  removeTab,
});

onUnmounted(() => {
  EventBus.off(COMMOND_TYPE_SELF_WRITE);
  EventBus.off(COMMOND_TYPE_D3_OP);
  EventBus.off(COMMOND_TYPE_2D_MESH);
  EventBus.off(COMMOND_TYPE_3D_MESH);
  EventBus.off(COMMOND_TYPE_MESH_SELECTED);
  EventBus.off(COMMOND_TYPE_MESH_CANCEL_SELECTED);
});
</script>
<style lang="less" scoped>
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}

.right-btns {
  position: fixed;
  right: 20px;
  top: 0;
  height: 100%;
  width: 300px;
  display: flex;
  justify-items: center;
  justify-content: center;
  align-items: center;
  align-content: center;

  .collspan-btns {
    position: absolute;
    top: 0;
    left: -30px;
    width: 60px;
    height: 100%;
    display: flex;
    justify-items: center;
    justify-content: center;
    align-items: center;
    align-content: center;
  }

  .open {
    left: -30px;
  }

  .close {
    left: calc(100% - 60px);
  }

  .inner {
    height: 600px;
    z-index: 1;

    .tabs-panel {
      width: 300px;
      height: 600px;

      border-radius: var(--el-card-border-radius);

      --el-bg-color-overlay: #e4e7ed1c;
      --el-border-color: var(--tool-panel-border-color);
      --el-fill-color-light: #e4e7ed1c;
      --el-border-color-light: var(--el-border-color);
      --el-color-primary: #00ff2b;
      --el-card-border-radius: 10px;

      background: var(--tool-panel-bg-color);

      :deep(.el-tabs__header) {
        border-top-left-radius: var(--el-card-border-radius);
        border-top-right-radius: var(--el-card-border-radius);
      }

      :deep(.el-tabs__item, .el-tabs__header) {
        border-top-left-radius: var(--el-card-border-radius);
        border-top-right-radius: var(--el-card-border-radius);
      }

      :deep(.el-tabs__content) {
        border-radius: var(--el-card-border-radius);
        padding: 5px;
        padding-top: 10px;

        .el-form-item__label {
          --el-text-color-regular: #fff;
        }
      }

      .config-form {
        width: 90%;
      }
    }
  }
}
</style>
  1. top.vue
javascript 复制代码
<template>
    <div class="top-btns">
      <div class="inner">
        <el-button-group>
          <el-button :round="index==0||index===tools.length-1" v-for="(item,index) in tools" type="success" :class="[item.key ==currentTop? 'selected':'']" 
           @click="commond(item)">
           <SvgIcon :src="item.icon" width="15px" height="15px" style="color:#fff;margin-right: 5px;"></SvgIcon>
           {{item.name}}
          </el-button>
        </el-button-group>
      </div>
    </div>
</template>
<script setup lang="ts">
import { useConfigStore } from '@/stores/config';
import { useStateStore } from '@/stores/state';
import { useToolStore } from '@/stores/tool';
import { commond } from '@/utils/CommondHandle';
import { computed } from 'vue';


//顶部的工具按钮信息
const tools = useToolStore().tools.top;

//顶部按钮是否自动隐藏
const topBarVisabled=  computed(()=>{
    return useConfigStore().topBarVisabled?'none':'block'
});

/**
 * 当前选中的顶部按钮
 */
const currentTop = computed(()=>{
   return useStateStore().currentTop;
})

</script>
<style lang="less" scoped>
    .top-btns{
      position: fixed;
      top: 0px;
      left: 0;
      height: 60px;
      width:100%;
      padding-top: 20px;

      .inner{
        margin: 0 auto;
        width: 396px;
        display: v-bind(topBarVisabled);

        .el-button{
            --el-button-bg-color:#67c23a40;
            --el-button-border-color:#67c23a40;
        }

        .selected{
            --el-button-bg-color:var(--el-color-success)
        }
      }
    }
    .top-btns:hover{
        .inner{
            display: block;
        }
    }
</style>

未完待续~~~~~

相关推荐
一勺菠萝丶2 小时前
管理后台使用手册在线预览与首次登录引导弹窗实现
java·前端·数据库
小村儿2 小时前
连载05-Claude Skill 不是抄模板:真正管用的 Skill,都是从实战里提炼出来的
前端·后端·ai编程
xiaotao1312 小时前
JS new 操作符完整执行过程
开发语言·前端·javascript·原型模式
robch2 小时前
python3 -m http.server 8001直接启动web服务类似 nginx
前端·nginx·http
吴声子夜歌2 小时前
ES6——数组的扩展详解
前端·javascript·es6
guhy fighting2 小时前
new Map,Array.from,Object.entries的作用以及使用方法
开发语言·前端·javascript
大漠_w3cpluscom2 小时前
CSS 技巧:CSS 单位使用指南
前端
STATICHIT静砸2 小时前
了解Monorepo结构
前端