Procházet zdrojové kódy

Merge branch 'xf_dev' of http://192.168.101.4:3000/jiaxiaoqiang/JG-ADMIN-TEMP into xf_dev

lupeng před 8 měsíci
rodič
revize
992bd5e0e1

docker/Dockerfile → docker/Dockerfile_xf


+ 3 - 0
package.json

@@ -44,6 +44,7 @@
     "@kjgl77/datav-vue3": "^1.7.3",
     "@smallwei/avue": "^3.3.3",
     "@types/smallwei__avue": "^3.0.5",
+    "@types/three": "^0.168.0",
     "@vueup/vue-quill": "1.0.0-alpha.40",
     "@vueuse/core": "^10.9.0",
     "@wangeditor/editor": "^5.1.23",
@@ -59,6 +60,7 @@
     "jspdf": "^2.5.1",
     "lodash-es": "^4.17.21",
     "luckyexcel": "^1.0.1",
+    "moment": "^2.30.1",
     "net": "^1.0.2",
     "nprogress": "^0.2.0",
     "path-browserify": "^1.0.1",
@@ -69,6 +71,7 @@
     "sockjs-client": "1.6.1",
     "sortablejs": "^1.15.2",
     "stompjs": "^2.3.3",
+    "three": "^0.168.0",
     "uuid": "^9.0.1",
     "v-scale-screen": "2.0.12",
     "vue": "^3.4.21",

binární
public/black_box.fbx


binární
public/images/111.gif


binární
public/images/a.png


binární
public/images/b.png


binární
public/images/c.png


binární
public/images/d.png


+ 36 - 0
src/api/bigScreen/index.ts

@@ -60,3 +60,39 @@ export function getStationState() {
     method: "post",
   });
 }
+
+//今日工排产
+export function getMaterialRate() {
+  return request({
+    url: "/api/v1/process/census/taskMaterial/rate",
+    method: "post",
+  });
+}
+//今日任务完成率
+export function getTaskCount() {
+  return request({
+    url: "/api/v1/process/census/task/count",
+    method: "post",
+  });
+}
+//工位任务完成率
+export function getTaskRate() {
+  return request({
+    url: "/api/v1/process/census/taskStation/rate",
+    method: "post",
+  });
+}
+//获取设备列表
+export function getDeviceList() {
+  return request({
+    url: "/api/v1/process/census/device/list",
+    method: "post",
+  });
+}
+//今日报故
+export function getTodayError() {
+  return request({
+    url: "/api/v1/process/census/today/fault",
+    method: "post",
+  });
+}

+ 22 - 0
src/api/screwdriver/index.ts

@@ -0,0 +1,22 @@
+import request from "@/utils/request";
+export function addScrewdriverTask(data: any) {
+  return request({
+    url: `/api/v1/screwdriverTask/add`,
+    method: "post",
+    data,
+  });
+}
+export function updateScrewdriverTask(data: any) {
+  return request({
+    url: `/api/v1/screwdriverTask/update`,
+    method: "post",
+    data,
+  });
+}
+export function deleteScrewdriverTask(data: any) {
+  return request({
+    url: `/api/v1/screwdriverTask/del`,
+    method: "post",
+    data,
+  });
+}

binární
src/assets/font/Furore.otf


+ 4 - 0
src/assets/font/font.css

@@ -0,0 +1,4 @@
+@font-face {
+  font-family: 'Furore';
+  src: url('./Furore.otf');
+}

binární
src/assets/images/cel.png


binární
src/assets/images/top.png


+ 68 - 0
src/components/ShowScroll/index.vue

@@ -0,0 +1,68 @@
+<template>
+  <div>
+    <slot></slot>
+  </div>
+</template>
+
+<script setup>
+const props = defineProps({
+  scrollRef: {
+    type: [Object, null],
+    required: true,
+  },
+  innerRef: {
+    type: [Object, null],
+    required: true,
+  },
+});
+const now = ref(0);
+const max = ref(0);
+const scrollContainer = ref(null);
+const innerRefs = ref(null);
+const setActive = () => {
+  if (innerRefs.value.clientHeight > props.scrollRef.wrapRef.clientHeight) {
+    max.value =
+      innerRefs.value.clientHeight - props.scrollRef.wrapRef.clientHeight + 20;
+    inputSlider();
+  }
+};
+const inputSlider = () => {
+  const scrollAmountPerSecond = window.innerHeight * 0.07; // 每秒滚动的距离
+  const totalScrollDistance = max.value; // 内容的总高度
+  const duration = (totalScrollDistance / scrollAmountPerSecond) * 1000; // 动画总时长(毫秒)
+
+  let startTime;
+
+  function animate(time) {
+    if (!startTime) startTime = time;
+    const elapsed = time - startTime;
+
+    // 计算当前位置
+    const progress = Math.min(elapsed / duration, 1);
+    now.value = progress * totalScrollDistance;
+    props.scrollRef.setScrollTop(now.value);
+
+    if (progress < 1) {
+      requestAnimationFrame(animate);
+    } else {
+      now.value = 0; // 动画结束后重置位置
+      props.scrollRef.setScrollTop(now.value);
+
+      // 重置起始时间并重新开始动画
+      startTime = time;
+      requestAnimationFrame(animate);
+    }
+  }
+
+  requestAnimationFrame(animate); // 启动动画
+};
+nextTick(() => {
+  scrollContainer.value = props.scrollRef;
+  innerRefs.value = props.innerRef;
+});
+defineExpose({
+  setActive,
+});
+</script>
+
+<style scoped></style>

+ 190 - 0
src/hooks/ThreeHelper.ts

@@ -0,0 +1,190 @@
+import * as THREE from "three";
+import {
+  AxesHelper,
+  BoxGeometry,
+  Mesh,
+  MeshLambertMaterial,
+  PerspectiveCamera,
+  Vector2,
+} from "three";
+import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
+import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer";
+import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass";
+import { OutlinePass } from "three/examples/jsm/postprocessing/OutlinePass";
+import { FBXLoader } from "three/examples/jsm/loaders/FBXLoader";
+
+export default class ThreeHelper {
+  width: number = 0;
+  height: number = 0;
+  group: THREE.Group;
+  geometry: THREE.BoxGeometry;
+  material: THREE.MeshLambertMaterial;
+  mesh: THREE.Mesh;
+  camera: THREE.PerspectiveCamera;
+  axesHelper: THREE.AxesHelper;
+  scene: THREE.Scene;
+  light: THREE.PointLight;
+  renderer: THREE.WebGLRenderer | null = null;
+
+  constructor() {
+    this.group = new THREE.Group();
+
+    this.geometry = new BoxGeometry(60, 90, 80);
+    this.material = new MeshLambertMaterial({
+      color: 0x009900,
+      transparent: true,
+      opacity: 0.5,
+    });
+    this.mesh = new Mesh(this.geometry, this.material);
+    this.group.add(this.mesh);
+
+    this.camera = this.creatCamera();
+    this.axesHelper = new AxesHelper(400);
+    this.light = this.creatLight();
+
+    this.scene = new THREE.Scene();
+    this.scene.add(this.group);
+    this.scene.add(this.axesHelper);
+    this.scene.add(this.light);
+  }
+
+  creatLight = () => {
+    const light = new THREE.PointLight(0xffffff, 1.0);
+    light.intensity = 300.0; //光照强度
+    light.decay = 0.0; //设置光源不随距离衰减
+    light.position.set(1000, 1000, 1000);
+    return light;
+  };
+
+  creatRenderer = (dom: HTMLElement) => {
+    this.width = dom.clientWidth;
+    this.height = dom.clientHeight;
+
+    const renderer: THREE.WebGLRenderer = new THREE.WebGLRenderer({
+      antialias: true,
+    });
+    renderer.setSize(this.width, this.height);
+    renderer.setClearColor(0x000000, 0.4);
+    this.renderer = renderer;
+
+    // 改变相机观察目标
+    this.camera.aspect = this.width / this.height;
+    this.camera.updateProjectionMatrix();
+
+    // 注意相机控件OrbitControls会影响lookAt设置,注意手动设置OrbitControls的目标参数
+
+    const controls = new OrbitControls(this.camera, this.renderer.domElement);
+    // controls.target = mesh.position;
+    // controls.update();
+    controls.addEventListener("change", () => {
+      this.renderer!.render(this.scene, this.camera);
+    });
+
+    // console.log(this.renderer, dom);
+
+    dom.appendChild(renderer.domElement);
+
+    this.renderer.render(this.scene, this.camera);
+  };
+
+  loadFBX = (src: string, finish: () => void) => {
+    const loader = new FBXLoader();
+    loader.load(
+      src,
+      (fbx) => {
+        console.log("loadFBX", fbx);
+        this.group.add(fbx);
+        this.renderer!.render(this.scene, this.camera);
+        finish();
+      },
+      (event) => {
+        console.log(
+          "loadFBX" + (event.loaded / event.total) * 100 + "% loaded"
+        );
+      },
+      (error) => {
+        console.log("loadFBX", error);
+        finish();
+      }
+    );
+  };
+
+  addOutline = (
+    width: number = this.width,
+    height: number = this.height,
+    model?: THREE.Object3D
+  ) => {
+    const composer = new EffectComposer(this.renderer!);
+    const renderPass = new RenderPass(this.scene, this.camera);
+    const outlinePass = new OutlinePass(
+      new Vector2(width, height),
+      this.scene,
+      this.camera
+    );
+    //模型描边颜色,默认白色
+    outlinePass.visibleEdgeColor.set(0xffff00);
+    //高亮发光描边厚度
+    outlinePass.edgeThickness = 4;
+    //高亮描边发光强度
+    outlinePass.edgeStrength = 6;
+    //模型闪烁频率控制,默认0不闪烁
+    outlinePass.pulsePeriod = 2;
+    composer.addPass(renderPass);
+    composer.addPass(outlinePass);
+    // composer.render();
+
+    const that = this;
+
+    this.renderer!.domElement.addEventListener("click", function (event) {
+      // .offsetY、.offsetX以canvas画布左上角为坐标原点,单位px
+      const px = event.offsetX;
+      const py = event.offsetY;
+      //屏幕坐标px、py转WebGL标准设备坐标x、y
+      //width、height表示canvas画布宽高度
+      const x = (px / width) * 2 - 1;
+      const y = -(py / height) * 2 + 1;
+      //创建一个射线投射器`Raycaster`
+      const raycaster = new THREE.Raycaster();
+      //.setFromCamera()计算射线投射器`Raycaster`的射线属性.ray
+      // 形象点说就是在点击位置创建一条射线,射线穿过的模型代表选中
+      raycaster.setFromCamera(new THREE.Vector2(x, y), that.camera);
+      //.intersectObjects([mesh1, mesh2, mesh3])对参数中的网格模型对象进行射线交叉计算
+      // 未选中对象返回空数组[],选中一个对象,数组1个元素,选中两个对象,数组两个元素
+      const toAddModelArray = [];
+      if (model) {
+        toAddModelArray.push(model);
+      }
+      const intersects = raycaster.intersectObjects([
+        that.mesh,
+        ...toAddModelArray,
+      ]);
+      console.log("射线器返回的对象", intersects);
+      // intersects.length大于0说明,说明选中了模型
+      if (intersects.length > 0) {
+        outlinePass.selectedObjects = [intersects[0].object];
+        composer.render();
+      }
+    });
+  };
+
+  resizeRender = (dom: HTMLElement) => {
+    this.width = dom.clientWidth;
+    this.height = dom.clientHeight;
+    this.renderer!.setSize(this.width, this.height);
+    this.camera.aspect = this.width / this.height;
+    this.camera.updateProjectionMatrix();
+    this.renderer!.render(this.scene, this.camera);
+  };
+
+  private creatCamera = () => {
+    const camera = new PerspectiveCamera(
+      75,
+      window.innerWidth / window.innerHeight,
+      0.1,
+      3000
+    );
+    camera.position.set(1000, 1000, 1000);
+    camera.lookAt(0, 0, 0);
+    return camera;
+  };
+}

+ 1 - 0
src/main.ts

@@ -2,6 +2,7 @@ import { createApp } from "vue";
 import App from "./App.vue";
 import router from "@/router";
 import print from "vue3-print-nb";
+import "@/assets/font/font.css";
 import { setupStore } from "@/store";
 import { setupDirective } from "@/directive";
 import { setupElIcons, setupI18n, setupPermission } from "@/plugins";

+ 4 - 1
src/router/index.ts

@@ -59,7 +59,10 @@ export const constantRoutes: RouteRecordRaw[] = [
     component: () =>
       import("@/views/report/statistics/screens/quality/index.vue"),
   },
-
+  {
+    path: "/lineScreen",
+    component: () => import("@/views/report/statistics/screens/line/index.vue"),
+  },
   // 外部链接
   // {
   //   path: "/external-link",

+ 1 - 1
src/styles/reset.scss

@@ -27,7 +27,7 @@ body {
   margin: 0;
   overflow: hidden;
   font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
-  "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
+    "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
   line-height: inherit;
   -moz-osx-font-smoothing: grayscale;
   -webkit-font-smoothing: antialiased;

+ 74 - 89
src/views/base/craftManagement/route/components/bottomTable.vue

@@ -52,7 +52,7 @@
     </el-dialog>
   </div>
 </template>
-<script  setup>
+<script setup>
 import { ref, getCurrentInstance } from "vue";
 import { useCrud } from "@/hooks/userCrud";
 import { getTableConfig } from "./configs";
@@ -64,7 +64,7 @@ import {
   getRouteMaxVersion,
   addBatch,
   addEsopBatch,
-  addCheckBatch
+  addCheckBatch,
 } from "@/api/craft/process/index";
 import SingleUpload from "@/components/Upload/SingleUpload.vue";
 
@@ -137,7 +137,7 @@ const startCreat = () => {
         commonTableRef.value?.mergeOption({
           searchShow: bomVersion.value ? false : true,
           selection: true,
-          reserveSelection : true,
+          reserveSelection: true,
         });
       });
     });
@@ -147,7 +147,7 @@ const startCreat = () => {
       commonTableRef.value?.startSelect();
       commonTableRef.value?.mergeOption({
         selection: true,
-        reserveSelection : true,
+        reserveSelection: true,
       });
     });
   } else if (props.tableType === "ESOP") {
@@ -159,7 +159,7 @@ const startCreat = () => {
       });
       commonTableRef.value?.mergeOption({
         selection: true,
-        reserveSelection : true,
+        reserveSelection: true,
       });
     });
   } else {
@@ -178,7 +178,6 @@ const onSortChange = () => {
   });
 };
 
-
 // ============公共弹窗table选择相关,物料采集等使用===============
 const commonTableRef = ref({});
 const commonTableType = ref("MARTERIAL_BOM");
@@ -192,17 +191,17 @@ const esopList = ref([]);
 const checkItem = ref({});
 const checkList = ref([]);
 const onSelectedFinish = (itemValue) => {
-  if(Object.keys(itemValue).length==0){
+  if (Object.keys(itemValue).length == 0) {
     return false;
   }
   itemRecordList.value = [];
-  itemRecord.value.itemRecordList=[];
+  itemRecord.value.itemRecordList = [];
   checkList.value = [];
   checkItem.value.checkList = [];
   esopItem.value.esopList = [];
   esopList.value = [];
   if (props.tableType === "wuliaocaiji") {
-    itemValue?.forEach((item,index)=>{
+    itemValue?.forEach((item, index) => {
       const recordItem = ref({});
       recordItem.value.itemName = item.bomMaterialName;
       recordItem.value.itemCode = item.bomMaterialCode;
@@ -214,27 +213,23 @@ const onSelectedFinish = (itemValue) => {
       recordItem.value.unit = item.unit;
       recordItem.value.isTrace = 1;
       itemRecordList.value.push(recordItem.value);
-      }
-
-    );
+    });
     itemRecord.value.operationId = route.params.id;
     itemRecord.value.itemRecordList = Array.from(itemRecordList.value);
-    addBatch(itemRecord.value).then(
-      (data)=>{
-        if(data.code=="200"){
-          dataList();
-          ElMessage({
-            message: data.msg,
-            type: "success",
-          });
-        }else {
-          ElMessage({
-            message: data.msg,
-            type: "error",
-          });
-        }
+    addBatch(itemRecord.value).then((data) => {
+      if (data.code == "200") {
+        dataList();
+        ElMessage({
+          message: data.msg,
+          type: "success",
+        });
+      } else {
+        ElMessage({
+          message: data.msg,
+          type: "error",
+        });
       }
-    )
+    });
     //form.value.unit = itemValue.unit;
   } else if (props.tableType === "dianjian") {
     /*form.value.checkName = itemValue.checkName;
@@ -244,43 +239,37 @@ const onSelectedFinish = (itemValue) => {
     form.value.upper = itemValue.upper;
     form.value.lower = itemValue.lower;
     form.value.unit = itemValue.unit;*/
-    itemValue?.forEach((item,index)=>{
-        const recordItem = ref({});
-        recordItem.value.operationId = route.params.id;
-        recordItem.value.checkName = item.checkName;
-        recordItem.value.checkCode = item.checkCode;
-        recordItem.value.content = item.content;
-        recordItem.value.standard = item.standard;
-        recordItem.value.upper = item.upper;
-        recordItem.value.lower = item.lower;
-        recordItem.value.unit = item.unit;
-        checkList.value.push(recordItem.value);
-      }
-
-    );
+    itemValue?.forEach((item, index) => {
+      const recordItem = ref({});
+      recordItem.value.operationId = route.params.id;
+      recordItem.value.checkName = item.checkName;
+      recordItem.value.checkCode = item.checkCode;
+      recordItem.value.content = item.content;
+      recordItem.value.standard = item.standard;
+      recordItem.value.upper = item.upper;
+      recordItem.value.lower = item.lower;
+      recordItem.value.unit = item.unit;
+      checkList.value.push(recordItem.value);
+    });
     checkItem.value.operationId = route.params.id;
     checkItem.value.checkList = Array.from(checkList.value);
-    addCheckBatch(checkItem.value).then(
-      (data)=>{
-
-        if(data.code=="200"){
-          dataList();
-          commonTableRef.value?.mergeOption({
-            reserveSelection : false,
-          });
-          ElMessage({
-            message: data.msg,
-            type: "success",
-          });
-        }else {
-          ElMessage({
-            message: data.msg,
-            type: "error",
-          });
-        }
+    addCheckBatch(checkItem.value).then((data) => {
+      if (data.code == "200") {
+        dataList();
+        commonTableRef.value?.mergeOption({
+          reserveSelection: false,
+        });
+        ElMessage({
+          message: data.msg,
+          type: "success",
+        });
+      } else {
+        ElMessage({
+          message: data.msg,
+          type: "error",
+        });
       }
-    )
-
+    });
   } else if (props.tableType === "ESOP") {
     /*form.value.filePath = itemValue.pdfPath;
     pdfPath.value = import.meta.env.VITE_APP_UPLOAD_URL + itemValue.pdfPath;
@@ -288,37 +277,33 @@ const onSelectedFinish = (itemValue) => {
     form.value.recordVersion = itemValue.drawingVersion;
     form.value.sortNum = itemValue.sort;
     form.value.title = itemValue.drawingTitle;*/
-    itemValue?.forEach((item,index)=>{
-        const recordItem = ref({});
-        recordItem.value.operationId = route.params.id;
-        recordItem.value.filePath = item.pdfPath;
-        recordItem.value.showAppointPageNum = 1;
-        recordItem.value.drawingCode = item.drawingCode;
-        recordItem.value.recordVersion = item.drawingVersion;
-        recordItem.value.sortNum = item.sort;
-        recordItem.value.title = item.drawingTitle;
-        esopList.value.push(recordItem.value);
-      }
-
-    );
+    itemValue?.forEach((item, index) => {
+      const recordItem = ref({});
+      recordItem.value.operationId = route.params.id;
+      recordItem.value.filePath = item.pdfPath;
+      recordItem.value.showAppointPageNum = 1;
+      recordItem.value.drawingCode = item.drawingCode;
+      recordItem.value.recordVersion = item.drawingVersion;
+      recordItem.value.sortNum = item.sort;
+      recordItem.value.title = item.drawingTitle;
+      esopList.value.push(recordItem.value);
+    });
     esopItem.value.operationId = route.params.id;
     esopItem.value.esopList = Array.from(esopList.value);
-    addEsopBatch(esopItem.value).then(
-      (data)=>{
-        if(data.code=="200"){
-          dataList();
-          ElMessage({
-            message: data.msg,
-            type: "success",
-          });
-        }else {
-          ElMessage({
-            message: data.msg,
-            type: "error",
-          });
-        }
+    addEsopBatch(esopItem.value).then((data) => {
+      if (data.code == "200") {
+        dataList();
+        ElMessage({
+          message: data.msg,
+          type: "success",
+        });
+      } else {
+        ElMessage({
+          message: data.msg,
+          type: "error",
+        });
       }
-    )
+    });
   }
 };
 

+ 4 - 0
src/views/base/craftManagement/route/components/configs.ts

@@ -318,4 +318,8 @@ export const comTypes: comType[] = [
     compentName: "工序表单",
     compentType: "operationExcel",
   },
+  {
+    compentName: "数据采集",
+    compentType: "screwdriver",
+  },
 ];

+ 2 - 1
src/views/base/craftManagement/route/components/processComponent.vue

@@ -176,7 +176,8 @@ const clickToolCom = (com, index) => {
     com.compentType === "mingpai" ||
     com.compentType === "duomeiticaiji" ||
     com.compentType === "tiaoshipipei" ||
-    com.compentType === "jingu"
+    com.compentType === "jingu" ||
+    com.compentType === "screwdriver"
   ) {
     isNoneedEdit.value = true;
   } else if (com.compentType === "ceshijilu") {

+ 0 - 1
src/views/base/excel/template/index.vue

@@ -82,7 +82,6 @@
                   ref="selectBom"
                   placeholder="请选择物料"
                   @click="startCreat"
-                  @focus="startCreat"
                 >
                   <el-option
                     v-for="item in formVlaue.materialList"

+ 278 - 0
src/views/device/screwdriver/index.vue

@@ -0,0 +1,278 @@
+<template>
+  <div class="mainContentBox">
+    <avue-crud
+      ref="crudRef"
+      v-model="form"
+      v-model:page="page"
+      v-model:search="search"
+      :data="data"
+      :option="option"
+      @row-save="createRow"
+      @row-update="updateRow"
+      @row-del="deleteRow"
+      @search-change="searchChange"
+      @search-reset="resetChange"
+      @size-change="dataList"
+      @current-change="dataList"
+      @selection-change="selectionChange"
+    >
+      <template #menu-left="{ size }">
+        <el-button type="primary" @click="addTask">新增</el-button>
+      </template>
+      <template #menu="{ size, row, index }">
+        <el-button @click="lookTask(row)" type="primary" link>查看</el-button>
+        <el-button @click="updateTask(row)" type="primary" link>修改</el-button>
+        <el-button @click="deleteTask(row.id)" type="danger" link
+          >删除</el-button
+        >
+      </template></avue-crud
+    >
+    <el-dialog
+      v-model="dialog.visible"
+      :title="dialog.title"
+      width="850px"
+      @close="dialog.visible = false"
+    >
+      <el-form
+        ref="ruleFormRef"
+        :model="ruleForm"
+        :rules="rules"
+        label-width="auto"
+        class="demo-ruleForm"
+      >
+        <el-form-item label="任务名称" prop="taskName">
+          <el-input :disabled="lookStatus" v-model="ruleForm.taskName" />
+        </el-form-item>
+        <el-form-item label="物料名称" prop="materialName">
+          <el-input
+            :disabled="lookStatus"
+            v-model="ruleForm.materialName"
+            @click="open"
+          />
+        </el-form-item>
+        <el-form-item label="物料编号" prop="materialNo">
+          <el-input
+            :disabled="lookStatus"
+            v-model="ruleForm.materialNo"
+            @click="open"
+          />
+        </el-form-item>
+        <el-form-item label="是否启用" prop="state">
+          <el-switch
+            :disabled="lookStatus"
+            v-model="ruleForm.state"
+            :active-value="1"
+            :inactive-value="0"
+          />
+        </el-form-item>
+      </el-form>
+
+      <el-tabs v-model="activeName" class="demo-tabs">
+        <el-tab-pane label="任务00" name="task0">
+          <el-input :disabled="lookStatus" v-model="ruleForm.task0" />
+        </el-tab-pane>
+        <el-tab-pane label="任务01" name="task1">
+          <el-input :disabled="lookStatus" v-model="ruleForm.task1"
+        /></el-tab-pane>
+        <el-tab-pane label="任务02" name="task2">
+          <el-input :disabled="lookStatus" v-model="ruleForm.task2"
+        /></el-tab-pane>
+        <el-tab-pane label="任务03" name="task3">
+          <el-input :disabled="lookStatus" v-model="ruleForm.task3"
+        /></el-tab-pane>
+        <el-tab-pane label="任务04" name="task4">
+          <el-input :disabled="lookStatus" v-model="ruleForm.task4"
+        /></el-tab-pane>
+        <el-tab-pane label="任务05" name="task5">
+          <el-input :disabled="lookStatus" v-model="ruleForm.task5"
+        /></el-tab-pane>
+        <el-tab-pane label="任务06" name="task6">
+          <el-input :disabled="lookStatus" v-model="ruleForm.task6"
+        /></el-tab-pane>
+        <el-tab-pane label="任务07" name="task7">
+          <el-input :disabled="lookStatus" v-model="ruleForm.task7"
+        /></el-tab-pane>
+      </el-tabs>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="dialog.visible = false">取消</el-button>
+          <el-button v-if="!lookStatus" type="primary" @click="submit">
+            确定
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+    <CommonTable
+      ref="ctableRef"
+      tableTitle="BOM添加"
+      tableType="MARTERIAL"
+      @selected-sure="onSelectedFinish"
+    />
+  </div>
+</template>
+<script setup>
+import { ref, getCurrentInstance } from "vue";
+import { useCrud } from "@/hooks/userCrud";
+import {
+  addScrewdriverTask,
+  updateScrewdriverTask,
+  deleteScrewdriverTask,
+} from "@/api/screwdriver";
+const activeName = ref("task0");
+const { form, data, option, search, page, toDeleteIds, Methords, Utils } =
+  useCrud({
+    src: "/api/v1/screwdriverTask",
+  });
+const { dataList, createRow, updateRow, deleteRow, searchChange, resetChange } =
+  Methords; //增删改查
+const { selectionChange, multipleDelete } = Methords;
+const multipleRow = ref(true);
+const ctableRef = ref(null);
+const rules = {
+  taskName: {
+    required: true,
+    trigger: "blur",
+  },
+  materialName: {
+    required: true,
+    trigger: "blur",
+  },
+  materialNo: {
+    required: true,
+    trigger: "blur",
+  },
+  state: {
+    required: true,
+    trigger: "blur",
+  },
+};
+const submit = async () => {
+  if (addStatus.value) {
+    const { data, code } = await addScrewdriverTask({
+      ...ruleForm.value,
+    });
+    dialog.visible = false;
+    if (code == "200") {
+      ElMessage.success("操作成功");
+      dataList();
+    }
+  } else {
+    const { data, code } = await updateScrewdriverTask({
+      ...ruleForm.value,
+    });
+    if (code == "200") {
+      ElMessage.success("操作成功");
+      dataList();
+    }
+    dialog.visible = false;
+  }
+};
+const open = () => {
+  if (ctableRef.value) {
+    ctableRef.value.startSelect();
+  }
+};
+const onSelectedFinish = (selectedValueList) => {
+  if (
+    Object.keys(selectedValueList).length === 0 ||
+    selectedValueList == undefined ||
+    selectedValueList == null ||
+    selectedValueList.length <= 0
+  ) {
+    ElMessage({
+      message: "没有选择任何物料",
+      type: "error",
+    });
+    return false;
+  }
+  ruleForm.value.materialName = selectedValueList.materialName;
+  ruleForm.value.materialNo = selectedValueList.materialCode;
+};
+const dialog = reactive({
+  title: "设备选择",
+  visible: false,
+});
+const ruleForm = ref({
+  materialName: "",
+  materialNo: "",
+  state: 0,
+  taskName: "",
+});
+const addStatus = ref(false);
+const lookStatus = ref(false);
+const resetForm = () => {
+  activeName.value = "task0";
+  ruleForm.value = {};
+};
+const addTask = () => {
+  resetForm();
+  lookStatus.value = false;
+  addStatus.value = true;
+  dialog.title = "新增任务";
+  dialog.visible = true;
+};
+const updateTask = (row) => {
+  resetForm();
+  lookStatus.value = false;
+  addStatus.value = false;
+  ruleForm.value = row;
+  dialog.title = "编辑任务";
+  dialog.visible = true;
+};
+const lookTask = (row) => {
+  resetForm();
+  addStatus.value = false;
+  lookStatus.value = true;
+  ruleForm.value = row;
+  dialog.title = "查看任务";
+  dialog.visible = true;
+};
+const deleteTask = async (id) => {
+  const { code } = await deleteScrewdriverTask({ id });
+  if (code == "200") {
+    ElMessage.success("操作成功");
+    dataList();
+  }
+};
+option.value = Object.assign(option.value, {
+  delBtn: false,
+  addBtn: false,
+  selection: false,
+  labelWidth: 150,
+  viewBtn: false,
+  editBtn: false,
+  column: [
+    {
+      label: "任务名称",
+      prop: "taskName",
+      addDisabled: true,
+      search: true,
+    },
+    {
+      label: "物料名称",
+      prop: "creator",
+      addDisabled: true,
+      display: false,
+    },
+    {
+      label: "物料编号",
+      addDisabled: true,
+      prop: "materialNo",
+    },
+    {
+      label: "创建人",
+      prop: "creator",
+      display: false,
+    },
+    {
+      label: "时间",
+      prop: "created",
+      display: false,
+    },
+  ],
+});
+onMounted(() => {
+  dataList();
+});
+</script>
+<style lang="scss" scoped></style>

+ 16 - 0
src/views/pro/traceability/components/updateExcel.vue

@@ -334,6 +334,22 @@ watch(
   () => {
     if (rowData.value.lookStatus == true) {
       options.value.edit = false;
+    } else {
+      options.value.edit = true;
+      options.value.opreaState = true;
+    }
+  },
+  { immediate: true, deep: true }
+);
+watch(
+  () => rowData.value.lookStatus,
+  (val) => {
+    if (val == true) {
+      options.value.opreaState = false;
+      excelKey1.value = !excelKey1.value;
+    } else {
+      options.value.opreaState = true;
+      excelKey1.value = !excelKey1.value;
     }
   },
   { immediate: true, deep: true }

+ 64 - 2
src/views/report/statistics/screens/common-header.vue

@@ -1,14 +1,42 @@
 <template>
-  <div class="screen-header">{{ title }}</div>
+  <div style="position: relative">
+    <div class="screen-header">{{ title }}</div>
+    <div class="right">
+      <div class="show-time">
+        <div class="time">
+          {{ times.date }}
+        </div>
+        <div class="date">
+          <span>
+            {{ times.week }}
+          </span>
+        </div>
+      </div>
+      <div class="temperature">32<span style="font-size: 1vw">℃</span></div>
+    </div>
+  </div>
 </template>
 
-<script lang="ts" setup>
+<script setup>
+import moment from "moment";
 defineProps({
   title: {
     type: String,
     default: "",
   },
 });
+const times = ref({ time: "", date: "", week: "" });
+const timer = ref(null);
+const getTime = () => {
+  timer.value = setInterval(() => {
+    times.value.time = moment().format("HH:mm:ss");
+    times.value.date = moment().format("YYYY/MM/DD");
+    times.value.week = "星期" + "日一二三四五六".charAt(new Date().getDay());
+  }, 1000);
+};
+onMounted(() => {
+  getTime();
+});
 </script>
 
 <style lang="scss" scoped>
@@ -25,4 +53,38 @@ defineProps({
   background-size: 100% 100%;
   background-position: center;
 }
+.right {
+  position: absolute;
+  right: 1.042vw;
+  top: 50%;
+  transform: translateY(-50%);
+  display: flex;
+  align-items: center;
+  font-family: "Furore";
+  .temperature {
+    margin-right: 0.78125vw;
+    font-size: 1.25vw;
+    font-weight: 500;
+    color: white;
+    span {
+      color: #babebe;
+      font-size: 0.625vw;
+      vertical-align: super;
+    }
+  }
+  .show-time {
+    margin-right: 1vw;
+    .time {
+      font-size: 1.25vw;
+      font-weight: 500;
+      color: white;
+    }
+    .date {
+      color: #babebe;
+      & span:nth-child(1) {
+        margin-right: 0.26vw;
+      }
+    }
+  }
+}
 </style>

+ 92 - 0
src/views/report/statistics/screens/common-headerB.vue

@@ -0,0 +1,92 @@
+<template>
+  <div style="position: relative">
+    <div class="screen-header">{{ title }}</div>
+    <div class="right">
+      <div class="show-time">
+        <div class="time">
+          {{ times.date }}
+        </div>
+        <div class="date">
+          <span>
+            {{ times.week }}
+          </span>
+        </div>
+      </div>
+      <div class="temperature">32<span style="font-size: 1vw">℃</span></div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import moment from "moment";
+defineProps({
+  title: {
+    type: String,
+    default: "",
+  },
+});
+const times = ref({ time: "", date: "", week: "" });
+const timer = ref(null);
+const getTime = () => {
+  timer.value = setInterval(() => {
+    times.value.time = moment().format("HH:mm:ss");
+    times.value.date = moment().format("YYYY/MM/DD");
+    times.value.week = "星期" + "日一二三四五六".charAt(new Date().getDay());
+  }, 1000);
+};
+onMounted(() => {
+  getTime();
+});
+</script>
+
+<style lang="scss" scoped>
+.screen-header {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 8vh;
+  font-weight: 600;
+  font-size: 4vh;
+  color: #ffffff;
+  line-height: 4.6vh;
+  background-image: url("@/assets/images/top.png");
+  background-size: 100% 30%;
+  background-position: bottom;
+  background-repeat: no-repeat;
+}
+.right {
+  position: absolute;
+  right: 1.042vw;
+  top: 50%;
+  transform: translateY(-50%);
+  display: flex;
+  align-items: center;
+  font-family: "Furore";
+  .temperature {
+    margin-right: 0.78125vw;
+    font-size: 1.25vw;
+    font-weight: 500;
+    color: white;
+    span {
+      color: #babebe;
+      font-size: 0.625vw;
+      vertical-align: super;
+    }
+  }
+  .show-time {
+    margin-right: 1vw;
+    .time {
+      font-size: 1.25vw;
+      font-weight: 500;
+      color: white;
+    }
+    .date {
+      color: #babebe;
+      font-size: 1vw;
+      & span:nth-child(1) {
+        margin-right: 0.26vw;
+      }
+    }
+  }
+}
+</style>

+ 688 - 0
src/views/report/statistics/screens/line/index.vue

@@ -0,0 +1,688 @@
+<template>
+  <div class="screen-container">
+    <common-headerB title="装调一体式智能生产线仿真大屏" />
+    <div class="body">
+      <div class="left bg">
+        <TitleHeaderB title="工位状态" />
+        <div class="totalInfo">
+          <div class="left">
+            <div class="text">{{ countData.dayRate }}</div>
+          </div>
+          <div class="right">
+            <div class="big">
+              <div class="leftBox">
+                <div class="num">{{ countData.dayPlan }}</div>
+                <div class="title">今日计划</div>
+              </div>
+              <div class="rightBox">
+                <div class="num">{{ countData.dayFinish }}</div>
+                <div class="title">今日完成</div>
+              </div>
+            </div>
+            <div class="small">
+              <div class="title">本周进度</div>
+              <div class="title">
+                {{ countData.weekFinish }} &nbsp;/
+                <span class="num">{{ countData.weekPlan }}</span>
+              </div>
+            </div>
+            <div class="small">
+              <div class="title">本月进度</div>
+              <div class="title">
+                {{ countData.monthFinish }} &nbsp;/
+                <span class="num">{{ countData.monthPlan }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="titleInfo">
+          <div class="infoItem">
+            <div class="leftItem">工位名称</div>
+            <div class="rightItem">完成率</div>
+          </div>
+        </div>
+        <ShowScroll
+          ref="ShowScrollRef1"
+          :scrollRef="scrollbarRef1"
+          :innerRef="innerRef1"
+        >
+          <el-scrollbar ref="scrollbarRef1" class="scrollbar">
+            <div ref="innerRef1">
+              <div
+                class="infoItem"
+                v-for="(item, index) in taskRateArray"
+                :key="index"
+              >
+                <div class="leftItem textComent itembg">
+                  {{ item.stationName }}
+                </div>
+                <div class="rightItem itembg">{{ item.dayRate }}</div>
+              </div>
+            </div>
+          </el-scrollbar>
+        </ShowScroll>
+      </div>
+      <div class="middle">
+        <div class="item">
+          <el-image src="/images/111.gif" style="width: 100%; height: 100%" />
+        </div>
+        <div class="item">
+          <TitleHeaderB title="自动化设备状态" />
+          <ShowScroll
+            ref="ShowScrollRef2"
+            :scrollRef="scrollbarRef2"
+            :innerRef="innerRef2"
+          >
+            <el-scrollbar ref="scrollbarRef2" class="scrollbar">
+              <div ref="innerRef2" class="infoContent">
+                <div
+                  class="deviceInfo"
+                  v-for="(item, index) in deviceArray"
+                  :key="index"
+                >
+                  <div class="img">
+                    <el-image
+                      :src="`/images/${getRandomLetter()}.png`"
+                      class="img"
+                    />
+                    <div
+                      class="imgbg"
+                      :class="{
+                        outlinebg: item.state == '离线',
+                        errorbg: item.state == '故障',
+                      }"
+                    ></div>
+                  </div>
+                  <div class="info">
+                    <div class="info1">{{ item.deviceName }}</div>
+                    <div class="info2">
+                      <div class="text2">
+                        今日稼动
+                        <span class="nums">{{ item.utilizationRate }}</span>
+                      </div>
+                      |
+                      <div class="text2">
+                        总稼动
+                        <span class="nums" style="color: white">{{
+                          item.totalRate
+                        }}</span>
+                      </div>
+                    </div>
+                    <div class="info3">
+                      <div class="ip">IP:{{ item.ip }}</div>
+                      <div
+                        class="infoState"
+                        :class="{
+                          red: item.state == '故障',
+                          white: item.state == '离线',
+                        }"
+                      >
+                        {{ item.state }}
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </el-scrollbar>
+          </ShowScroll>
+        </div>
+        <div class="item">
+          <TitleHeaderB title="不合格情况" />
+          <div class="scrollbar" style="position: relative">
+            <div id="charts2"></div>
+          </div>
+        </div>
+      </div>
+      <div class="right bg">
+        <div class="top">
+          <TitleHeaderB title="今日排产" />
+          <div class="titleInfo">
+            <div class="infoItem">
+              <div class="leftItem1">产品名称</div>
+              <div class="midItem1">今日排产</div>
+              <div class="rightItem1">完成率</div>
+            </div>
+          </div>
+          <ShowScroll
+            ref="ShowScrollRef3"
+            :scrollRef="scrollbarRef3"
+            :innerRef="innerRef3"
+          >
+            <el-scrollbar
+              class="scrollbar"
+              ref="scrollbarRef3"
+              style="height: 53vh"
+            >
+              <div ref="innerRef3">
+                <div
+                  class="infoItem"
+                  v-for="(item, index) in materialArray"
+                  :key="index"
+                >
+                  <div class="leftItem1 textComent itembg">
+                    {{ item.materialName }}
+                  </div>
+                  <div class="midItem1 itembg">{{ item.planNum }}</div>
+                  <div class="rightItem1 itembg">{{ item.dayRate }}</div>
+                </div>
+              </div>
+            </el-scrollbar>
+          </ShowScroll>
+        </div>
+        <div class="bottom">
+          <TitleHeaderB title="今日报故" />
+          <div class="titleInfo">
+            <div class="infoItem">
+              <div class="leftItem">工位名称</div>
+              <div class="rightItem">报故数量</div>
+            </div>
+          </div>
+          <ShowScroll
+            ref="ShowScrollRef4"
+            :scrollRef="scrollbarRef4"
+            :innerRef="innerRef4"
+          >
+            <el-scrollbar
+              ref="scrollbarRef4"
+              class="scrollbar"
+              style="height: 20vh"
+            >
+              <div ref="innerRef4">
+                <div
+                  class="infoItem"
+                  v-for="(item, index) in todayErrorArray"
+                  :key="index"
+                >
+                  <div class="leftItem textComent itembg">
+                    {{ item.materialName }}
+                  </div>
+                  <div class="rightItem itembg" style="color: #db4848">
+                    {{ item.unqualifiedNum }}
+                  </div>
+                </div>
+              </div>
+            </el-scrollbar>
+          </ShowScroll>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import CommonHeaderB from "@/views/report/statistics/screens/common-headerB.vue";
+import TitleHeaderB from "../titleHeaderB.vue";
+import ShowScroll from "@/components/ShowScroll/index.vue";
+import * as echarts from "echarts";
+import {
+  getFaultCount,
+  getTaskCount,
+  getTaskRate,
+  getDeviceList,
+  getMaterialRate,
+  getTodayError,
+} from "@/api/bigScreen";
+import Middle3D from "./middle3D.vue";
+import moment from "moment";
+const fontSize = ref(0);
+const scrollbarRef1 = ref(null);
+const ShowScrollRef1 = ref(null);
+const innerRef1 = ref(null);
+const scrollbarRef2 = ref(null);
+const ShowScrollRef2 = ref(null);
+const innerRef2 = ref(null);
+const scrollbarRef3 = ref(null);
+const ShowScrollRef3 = ref(null);
+const innerRef3 = ref(null);
+const scrollbarRef4 = ref(null);
+const ShowScrollRef4 = ref(null);
+const innerRef4 = ref(null);
+const setFontSize = () => {
+  fontSize.value = (window.innerHeight / 100) * 1.55;
+};
+const charts2 = shallowRef(null);
+const chartsData2 = ref([]);
+const getRandomLetter = () => {
+  const letters = ["a", "b", "c", "d"];
+  const randomIndex = Math.floor(Math.random() * letters.length);
+  return letters[randomIndex];
+};
+const getData4 = async () => {
+  //不合格品统计
+  const { data } = await getFaultCount();
+  chartsData2.value = data;
+};
+const countData = ref({});
+//获取今日任务完成相关统计
+const getTodayData = async () => {
+  const { data } = await getTaskCount();
+  countData.value = data;
+};
+const taskRateArray = ref([]);
+//获取工位任务完成率
+const getRateArray = async () => {
+  const { data } = await getTaskRate();
+  taskRateArray.value = data;
+  nextTick(() => {
+    ShowScrollRef1.value.setActive();
+  });
+};
+const deviceArray = ref([]);
+//获取设备列表
+const getDevice = async () => {
+  const { data } = await getDeviceList();
+  deviceArray.value = data;
+  nextTick(() => {
+    ShowScrollRef2.value.setActive();
+  });
+};
+const materialArray = ref([]);
+const getMaterialArray = async () => {
+  const { data } = await getMaterialRate();
+  materialArray.value = data;
+  nextTick(() => {
+    ShowScrollRef3.value.setActive();
+  });
+};
+//今日报故
+const todayErrorArray = ref([]);
+const getTodayErrorArray = async () => {
+  const { data } = await getTodayError();
+  todayErrorArray.value = data;
+  nextTick(() => {
+    ShowScrollRef4.value.setActive();
+  });
+};
+const option1 = {
+  xAxis: {
+    type: "category",
+    data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
+    axisLabel: {
+      fontSize: 12,
+    },
+  },
+  tooltip: {
+    trigger: "axis",
+    backgroundColor: "#00000030",
+    textStyle: {
+      color: "#fff",
+    },
+    formatter: function (param) {
+      const array = [
+        moment().year() +
+          "年" +
+          param[0].name +
+          "&nbsp;&nbsp;" +
+          param[0].value +
+          "<br/>",
+      ];
+      for (
+        let i = 0;
+        i < chartsData2.value.faultList[param[0].dataIndex].length;
+        i++
+      ) {
+        array.push(
+          ` <span style='width: 10vw'>${chartsData2.value.faultList[param[0].dataIndex][i].materialName}</span><span style='color:#D75656;'>&nbsp;&nbsp;${chartsData2.value.faultList[param[0].dataIndex][i].unqualifiedNum}</span>` +
+            "<br/>"
+        );
+      }
+      return array.join("");
+    },
+  },
+  grid: {
+    left: "3%",
+    right: "3%",
+    top: "12%",
+    bottom: "15%",
+  },
+  yAxis: {
+    type: "value",
+    axisLabel: {
+      fontSize: 12,
+    },
+    minInterval: 1,
+  },
+  series: [
+    {
+      data: [820, 932, 901, 934, 1290, 1330, 1320],
+      type: "line",
+      stack: "Total",
+      label: {
+        show: true,
+        position: "top",
+        fontSize: 20,
+        color: "white",
+      },
+      itemStyle: {
+        color: "#D7565680",
+      },
+      areaStyle: {
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          {
+            offset: 0,
+            color: "#D7565680",
+          },
+          {
+            offset: 1,
+            color: "#D7565680",
+          },
+        ]),
+      },
+    },
+  ],
+  textStyle: {
+    fontSize: fontSize.value,
+  },
+};
+const setChart1Option = () => {
+  const op1 = { ...option1 };
+  if (chartsData2.value && charts2.value) {
+    op1.series[0].data = chartsData2.value.countList;
+    op1.series[0].label.fontSize = fontSize.value;
+    op1.xAxis.data = chartsData2.value.monthList;
+    op1.yAxis.axisLabel.fontSize = fontSize.value;
+    op1.xAxis.axisLabel.fontSize = fontSize.value;
+    op1.tooltip.textStyle.fontSize = fontSize.value;
+    charts2.value.setOption(op1, true);
+  }
+};
+onMounted(async () => {
+  setFontSize();
+  await getData4();
+  getTodayData();
+  getRateArray();
+  getDevice();
+  getMaterialArray();
+  getTodayErrorArray();
+  charts2.value = echarts.init(document.getElementById("charts2"));
+  setChart1Option();
+  window.addEventListener("resize", function () {
+    charts2.value.resize();
+  });
+});
+</script>
+
+<style lang="scss" scoped>
+.red {
+  color: red !important;
+}
+.white {
+  color: white !important;
+}
+.titleInfo {
+  height: 3vh;
+}
+.scrollbar {
+  padding-top: 0.5vh;
+  height: 58vh;
+}
+#charts2 {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+}
+.screen-container {
+  width: 100vw;
+  height: 100vh;
+  background-image: url("@/assets/images/screen_bg_task.png");
+  background-size: cover;
+  background-position: center;
+  overflow: hidden;
+}
+.bg {
+  background-color: rgba(0, 0, 0, 0.4);
+}
+.body {
+  padding-top: 1vh;
+  width: 100vw;
+  height: 90vh;
+  display: flex;
+  justify-content: space-evenly;
+  .left {
+    padding: 1vh;
+    width: 18vw;
+    height: 90vh;
+    .totalInfo {
+      height: 20vh;
+      display: flex;
+      padding: 1vh;
+      .left {
+        width: 7vw;
+        height: 7vw;
+        background-image: url("@/assets/images/cel.png");
+        background-size: 100% 100%;
+        background-position: center;
+        position: relative;
+        .text {
+          width: 7vw;
+          height: 7vw;
+          top: 0;
+          left: 0;
+          color: white;
+          text-align: center;
+          line-height: 6vw;
+          font-size: 2.3vh;
+          padding-right: 1vw;
+        }
+      }
+      .right {
+        width: 9vw;
+        height: 20vh;
+        display: flex;
+        flex-direction: column;
+        justify-content: space-evenly;
+        .title {
+          font-size: 1.5vh;
+          color: white;
+          text-align: center;
+        }
+        .big {
+          height: 8vh;
+          width: 9vw;
+          display: flex;
+          .num {
+            font-size: 3vh;
+            font-weight: bolder;
+            color: white;
+            text-align: center;
+          }
+
+          .leftBox {
+            width: 4.5vw;
+            height: 8vh;
+            border-right: 0.1vw solid white;
+          }
+          .rightBox {
+            width: 4.5vw;
+            height: 8vh;
+          }
+        }
+        .small {
+          height: 4vh;
+          display: flex;
+          justify-content: space-between;
+          .num {
+            color: rgb(8, 174, 199);
+          }
+        }
+      }
+    }
+  }
+  .middle {
+    width: 60vw;
+    height: 90vh;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    .item {
+      height: 29vh;
+      padding: 1vh;
+      width: 100%;
+      background-color: rgba(0, 0, 0, 0.4);
+      .scrollbar {
+        height: 23.9vh;
+        width: 100%;
+        .infoContent {
+          width: 100%;
+          padding: 1vh;
+          display: grid;
+          gap: 0.6vw;
+          grid-template-columns: repeat(3, 1fr);
+          .deviceInfo {
+            width: 19vw;
+            height: 10vh;
+            display: flex;
+            background-color: rgba(255, 255, 255, 0.1);
+            clip-path: polygon(0% 0%, 85% 0%, 100% 25%, 100% 100%, 0% 100%);
+            .img {
+              height: 10vh;
+              width: 7vw;
+              position: relative;
+              .imgbg {
+                position: absolute;
+                height: 10vh;
+                width: 7vw;
+                top: 0;
+                left: 0;
+                z-index: 2;
+                background-color: #06ffa520;
+                border: 0.4vh solid #06ffa5;
+              }
+              .outlinebg {
+                background-color: #ffffff20 !important;
+                border: 0.4vh solid #fff !important;
+              }
+              .errorbg {
+                background-color: #db484820 !important;
+                border: 0.4vh solid #db4848 !important;
+              }
+            }
+            .info {
+              height: 10vh;
+              width: 12vw;
+              display: flex;
+              flex-direction: column;
+              justify-content: space-between;
+              padding-top: 1vh;
+
+              .info1 {
+                padding-left: 0.5vw;
+                height: 2vh;
+                font-size: 2vh;
+                line-height: 2vh;
+                font-weight: 600;
+                color: white;
+              }
+              .info2 {
+                height: 2vh;
+                padding-left: 0.5vw;
+                display: flex;
+                justify-content: space-between;
+                align-items: center;
+                color: white;
+                padding-right: 1vw;
+                .text2 {
+                  font-size: 1.5vh;
+                  .nums {
+                    color: #0ae0ff;
+                    font-weight: 600;
+                  }
+                }
+              }
+              .info3 {
+                height: 2vh;
+                font-size: 2vh;
+                background-color: rgba(255, 255, 255, 0.1);
+                display: flex;
+                padding-left: 0.5vw;
+                padding-right: 1vw;
+                justify-content: space-between;
+                .ip {
+                  font-size: 1.6vh;
+                  line-height: 2vh;
+                  color: white;
+                }
+                .infoState {
+                  font-size: 1.6vh;
+                  line-height: 2vh;
+                  color: #06ffa5;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+  .right {
+    width: 18vw;
+    height: 90vh;
+    display: flex;
+    flex-direction: column;
+    .top {
+      height: 61vh;
+      padding: 1vh;
+    }
+    .bottom {
+      height: 29vh;
+      padding: 1vh;
+    }
+  }
+}
+.textComent {
+  white-space: nowrap; /* 不允许换行 */
+  overflow: hidden; /* 超出长度时隐藏 */
+  text-overflow: ellipsis; /* 超出部分显示省略号 */
+}
+.infoItem {
+  height: 4vh;
+  margin-bottom: 0.4vh;
+  display: flex;
+  .leftItem {
+    flex: 1;
+    height: 4vh;
+    font-size: 1.8vh;
+    line-height: 4vh;
+    color: white;
+    text-align: center;
+  }
+  .rightItem {
+    width: 25%;
+    height: 4vh;
+    line-height: 4vh;
+    margin-left: 0.3vw;
+    font-size: 1.8vh;
+    color: white;
+    text-align: center;
+  }
+  .leftItem1 {
+    flex: 1;
+    height: 4vh;
+    font-size: 1.8vh;
+    line-height: 4vh;
+    color: white;
+    text-align: center;
+  }
+  .midItem1 {
+    width: 25%;
+    height: 4vh;
+    line-height: 4vh;
+    margin-left: 0.3vw;
+    font-size: 1.8vh;
+    color: white;
+    text-align: center;
+  }
+  .rightItem1 {
+    width: 20%;
+    height: 4vh;
+    line-height: 4vh;
+    margin-left: 0.3vw;
+    font-size: 1.8vh;
+    color: white;
+    text-align: center;
+  }
+}
+.itembg {
+  background-color: #46464635;
+}
+</style>

+ 47 - 0
src/views/report/statistics/screens/line/middle3D.vue

@@ -0,0 +1,47 @@
+<template>
+  <div ref="containerRef" class="middle3D"></div>
+</template>
+
+<script lang="ts" setup>
+import ThreeHelper from "@/hooks/ThreeHelper";
+import * as THREE from "three";
+
+const containerRef = ref<HTMLDivElement>(null);
+
+const threeHelper = new ThreeHelper();
+
+onMounted(() => {
+  threeHelper.creatRenderer(containerRef.value);
+
+  threeHelper.loadFBX("/black_box.fbx", () => {
+    threeHelper.scene.traverse((child) => {
+      if (child instanceof THREE.Mesh) {
+        console.log("traverse", child.material);
+        if (child.material.length > 1) {
+          child.material[0].color.set(0x00ff00);
+          child.material[1].color.set(0x00ff00);
+          child.material[2].color.set(0x00ff00);
+          child.material[3].color.set(0x00ff00);
+          threeHelper.resizeRender(containerRef.value);
+        }
+      }
+    });
+  });
+
+  // threeHelper.addOutline();
+});
+
+window.addEventListener("resize", () => {
+  threeHelper.resizeRender(containerRef.value);
+  // threeHelper.addOutline();
+});
+</script>
+
+<style lang="scss" scoped>
+.middle3D {
+  height: 29vh;
+  width: 100%;
+  padding: 0 !important;
+  border: 2px solid red;
+}
+</style>

+ 1 - 1
src/views/report/statistics/screens/titleHeader.vue

@@ -50,7 +50,7 @@ defineProps({
       align-items: center;
       justify-content: center;
       margin-right: 0.8vw;
-      border: 1px solid rgba(255, 255, 255, 0.3);
+      border: 0.1vh solid rgba(255, 255, 255, 0.3);
       .colorRound {
         height: 1.18vh;
         width: 1.18vh;

+ 124 - 0
src/views/report/statistics/screens/titleHeaderB.vue

@@ -0,0 +1,124 @@
+<template>
+  <div class="bodys">
+    <div class="info">
+      <div class="round">
+        <div style="left: 0">[</div>
+        <div style="right: 0">]</div>
+        <div class="box"></div>
+      </div>
+      <div class="text">{{ title }}</div>
+    </div>
+    <div class="nextline">
+      <div class="dian1">
+        <div class="lines"></div>
+      </div>
+      <div class="line">
+        <div class="lines"></div>
+      </div>
+      <div class="dian2">
+        <div class="lines"></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+defineProps({
+  title: {
+    type: String,
+    default: "",
+  },
+  color: {
+    type: String,
+    default: "#18FEFE",
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.bodys {
+  text-align: center;
+  display: flex;
+  flex-direction: column;
+  height: 4.1vh;
+  width: 100%;
+  .nextline {
+    display: flex;
+    align-items: center;
+  }
+  .info {
+    display: flex;
+    align-items: center;
+    .text {
+      color: white;
+      font-size: 2.5vh;
+      margin-left: 0.3vw;
+    }
+    .round {
+      height: 2.4vh;
+      width: 2vw;
+      font-size: 2.6vh;
+      line-height: 2.6vh;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      padding: 0 0.3vw;
+      color: white;
+      position: relative;
+      transform: translateY(-0.31vh);
+      div {
+        color: #18fefe;
+        position: absolute;
+      }
+      .box {
+        width: 0.5vw;
+        height: 0.5vw;
+        background-color: #18fefe;
+      }
+    }
+  }
+  .line {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-right: 0.5vw;
+    .lines {
+      width: 100%;
+      border: 1px solid rgba(255, 255, 255, 0.2);
+      height: 0vh;
+      border-top: 0;
+      border-right: 0;
+      border-left: 0;
+    }
+  }
+  .dian1 {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 2vw;
+    .lines {
+      width: 2vw;
+      border: 0.4vh solid rgba(255, 255, 255, 1);
+      height: 0vh;
+      border-top: 0;
+      border-right: 0;
+      border-left: 0;
+    }
+  }
+  .dian2 {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 0.5vw;
+    .lines {
+      width: 0.5vw;
+      border: 0.3vh solid rgb(24, 254, 254);
+      height: 0vh;
+      border-top: 0;
+      border-right: 0;
+      border-left: 0;
+    }
+  }
+}
+</style>