chao 3 weeks ago
parent
commit
a11324f766

File diff suppressed because it is too large
+ 716 - 1
package-lock.json


+ 7 - 1
package.json

@@ -9,9 +9,15 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@codemirror/lang-python": "^6.2.1",
+    "@microsoft/fetch-event-source": "^2.0.1",
+    "codemirror": "^6.0.1",
     "element-plus": "^2.10.1",
+    "markdown-it": "^14.1.0",
     "three": "^0.177.0",
-    "vue": "^3.4.21"
+    "uuid": "^11.1.0",
+    "vue": "^3.4.21",
+    "vue-socket.io": "^3.0.10"
   },
   "devDependencies": {
     "@types/three": "^0.177.0",

+ 4 - 0
src/assets/images/send_btn.svg

@@ -0,0 +1,4 @@
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect width="32" height="32" rx="16" fill="#68A1FD"/>
+<path d="M13.188 23.2586C12.9758 23.2586 12.7723 23.1954 12.6223 23.0829C12.4723 22.9703 12.388 22.8177 12.388 22.6586V17.1806C12.388 17.0006 12.496 16.8296 12.684 16.7156L19.164 12.7766C19.3289 12.6804 19.5376 12.6365 19.7452 12.6543C19.9527 12.6721 20.1428 12.7502 20.2746 12.8718C20.4064 12.9934 20.4694 13.1488 20.4503 13.3049C20.4311 13.4609 20.3312 13.6051 20.172 13.7066L13.988 17.4656V20.8556L15.932 18.9116C16.168 18.6746 16.604 18.6026 16.952 18.7436L20.176 20.0396L24 9.74359C24.076 9.54259 23.92 9.41659 23.848 9.37159C23.776 9.32659 23.588 9.23359 23.336 9.32059L8.83598 14.4236L10.716 15.2336C11.1 15.3986 11.232 15.7646 11.012 16.0526C10.792 16.3406 10.304 16.4396 9.91998 16.2746L6.70798 14.8916C6.58077 14.837 6.47612 14.7571 6.40555 14.6608C6.33497 14.5645 6.30121 14.4554 6.30798 14.3456C6.31998 14.1206 6.49598 13.9226 6.76798 13.8266L22.656 8.23459C23.4 7.97359 24.252 8.05759 24.88 8.45659C25.1856 8.64798 25.4107 8.90178 25.5279 9.1872C25.645 9.47263 25.6492 9.77741 25.54 10.0646L21.444 21.0836C21.4117 21.1701 21.3539 21.2501 21.2748 21.3176C21.1958 21.385 21.0975 21.4383 20.9872 21.4735C20.877 21.5086 20.7576 21.5247 20.638 21.5206C20.5183 21.5165 20.4014 21.4922 20.296 21.4496L16.8 20.0426L13.828 23.0186C13.672 23.1716 13.432 23.2586 13.188 23.2586Z" fill="white"/>
+</svg>

BIN
src/assets/images/top1.png


BIN
src/assets/images/top2.png


BIN
src/assets/images/top3.png


+ 59 - 0
src/components/BuildingInfoPanel.vue

@@ -0,0 +1,59 @@
+<template>
+  <div v-if="visible" class="building-info-panel">
+    <h3>{{ info.name }}</h3>
+    <p><strong>功能:</strong> {{ info.function }}</p>
+    <p><strong>开放时间:</strong> {{ info.openTime }}</p>
+    <p><strong>描述:</strong> {{ info.description }}</p>
+    <button @click="visible = false">关闭</button>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+
+const visible = ref(false);
+type BuildingInfo = {
+  name: string;
+  function: string;
+  openTime: string;
+  description?: string;
+};
+
+const info = ref<Partial<BuildingInfo>>({});
+
+const show = (buildingInfo) => {
+  info.value = buildingInfo;
+  visible.value = true;
+};
+
+defineExpose({ show });
+</script>
+
+<style scoped>
+.building-info-panel {
+  position: fixed;
+  top: 20px;
+  right: 20px;
+  background: rgba(255, 255, 255, 0.9);
+  padding: 15px;
+  border-radius: 8px;
+  box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
+  max-width: 300px;
+  z-index: 100;
+}
+
+.building-info-panel h3 {
+  margin-top: 0;
+  color: #1a3b6e;
+}
+
+.building-info-panel button {
+  margin-top: 10px;
+  padding: 5px 10px;
+  background: #1a3b6e;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+</style>

+ 92 - 0
src/hooks/useSockets.ts

@@ -0,0 +1,92 @@
+
+import { onBeforeUnmount, ref } from 'vue';
+
+
+// function useMultipleWebSockets(urls: any) {
+
+//   // 初始化所有连接
+//   urls.forEach((url: string) => {
+//     sockets.value[url] = new WebSocket(url);
+
+//     sockets.value[url].onopen = () => {
+//       console.log(`WebSocket connected to ${url}`);
+//       status.value[url] = "connected";
+//     };
+
+//     sockets.value[url].onmessage = (event:any) => {
+//       // console.log(`Message from ${url}:`, event.data);
+//       sendData.value[url] = event.data;
+//       // 处理不同来源的消息
+//     };
+
+//     sockets.value[url].onerror = (error:any) => {
+//       console.error(`WebSocket error on ${url}:`, error);
+//     };
+
+//     sockets.value[url].onclose = () => {
+//       console.log(`WebSocket disconnected from ${url}`);
+//       status.value[url] = "disconnected";
+//     };
+//   });
+
+//   // 组件卸载时关闭所有连接
+//   onBeforeUnmount(() => {
+//     Object.values(sockets).forEach(socket => {
+//       if (socket.readyState === WebSocket.OPEN) {
+//         socket.close();
+//       }
+//     });
+//   });
+
+//   // 发送消息到指定连接
+//   const send = (url: any, message: any) => {
+//     if (sockets.value[url] && sockets.value[url].readyState === WebSocket.OPEN) {
+//       sockets.value[url].send(JSON.stringify(message));
+//     }
+//   };
+
+//   return { sockets, send, sendData, status };
+// }
+
+const useSockets = () => {
+  const sockets = ref<{ [key: string]: WebSocket }>({});
+  const connectedStatus = ref<{ [key: string]: string }>({});
+
+  const connected = (url: any) => {
+    return new Promise((resolve, reject) => {
+      connectedStatus.value[url] = "connecting";
+      if (sockets.value[url]) {
+        resolve(sockets.value[url]);
+        return;
+      }
+      try {
+        const ws = new WebSocket(url) || "";
+        ws.onopen = () => {
+          sockets.value[url] = ws;
+          connectedStatus.value[url] = "connected";
+          resolve(ws);
+        };
+        ws.onclose = (error: any) => {
+          connectedStatus.value[url] = "disconnected";
+          reject(error);
+        };
+        ws.onerror = (error: any) => {
+          connectedStatus.value[url] = "error";
+          reject(error);
+        }
+      }catch (error) {
+        console.log(error)
+        reject(error);
+      }
+    });
+  };
+  onBeforeUnmount(() => {
+    Object.values(sockets.value).forEach(socket => {
+      socket.close();
+    });
+  });
+
+  return { sockets, connectedStatus, connected };
+};
+
+export default useSockets;

+ 473 - 52
src/pages/Entry/components/MapChart/index.vue

@@ -1,34 +1,37 @@
 <script setup lang="ts">
 import { ref, onMounted, onBeforeUnmount } from 'vue';
+import BuildingInfoPanel from '../../../../components/BuildingInfoPanel.vue';
 import * as THREE from 'three';
 import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 import { FontLoader } from 'three/addons/loaders/FontLoader.js';
 import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
-
-
+const infoPanel = ref<InstanceType<typeof BuildingInfoPanel> | null>(null);
+const raycaster = new THREE.Raycaster()
+const mouse = new THREE.Vector2();
 // 添加字体加载器
 const fontLoader = new FontLoader();
 const container = ref<HTMLDivElement | null>(null);
 const width = 600;
 const height = 400;
+const clickableBuildings: any = []; // 存储所有可点击建筑物
 
 // 场景变量
 const scene = new THREE.Scene();
-const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
+const camera = new THREE.PerspectiveCamera(100, window.innerWidth / window.innerHeight, 0.1, 1000);
 const renderer = new THREE.WebGLRenderer({ antialias: true });
 let controls: any = null;
 let animateId: any = null;
 
 // 初始化场景
-const initScene = () => {
+const initScene = (width: number, height: number) => {
   // 设置渲染器
-  renderer.setSize(width, height);
+  renderer.setSize(width, width * 2 / 3);
   renderer.setClearColor(0x87ceeb); // 天空蓝色背景
   renderer.shadowMap.enabled = true;
   container.value && container.value.appendChild(renderer.domElement);
 
   // 设置相机位置
-  camera.position.set(0, 10, 100);
+  camera.position.set(0, 30, 60);
 
   // 添加轨道控制器
   controls = new OrbitControls(camera, renderer.domElement);
@@ -66,40 +69,152 @@ const initScene = () => {
 
   // 创建椭圆操场
   createEllipticalPlayground(0, 0.02, 0);
+
+  // 添加篮球场
+  createBasketballCourt(-33, 0.02, -16);
+  createBasketballCourt(-33, 0.02, 0);
+  createBasketballCourt(-33, 0.02, 16);
+};
+
+// 处理鼠标点击事件
+const handleClick = (event: any) => {
+  // 将鼠标位置归一化为设备坐标 (-1到+1)
+  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
+  mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
+
+  // 更新射线投射器
+  raycaster.setFromCamera(mouse, camera);
+  // console.log(clickableBuildings);
+  // 计算与射线相交的物体
+  const intersects = raycaster.intersectObjects(clickableBuildings);
+
+  if (intersects.length > 0) {
+    // 找到第一个被点击的建筑物
+    const clickedBuilding = intersects[0].object;
+
+    // 执行点击后的操作
+    onBuildingClick(clickedBuilding);
+  }
+};
+
+// 建筑物点击回调
+const onBuildingClick = (building: any) => {
+  console.log("点击了建筑物:", building.userData.buildingInfo);
+  // 这里可以显示信息面板、高亮建筑物等
+  highlightBuilding(building);
+  showBuildingInfo(building.userData.buildingInfo);
+};
+
+// 高亮建筑物
+const highlightBuilding = (building: any) => {
+  // 先重置所有高亮
+  clickableBuildings.forEach((b: any) => {
+    if (b.userData.originalMaterial) {
+      b.material = b.userData.originalMaterial;
+    }
+  });
+
+  // 保存原始材质
+  if (!building.userData.originalMaterial) {
+    building.userData.originalMaterial = building.material;
+  }
+
+  // 创建高亮材质
+  const highlightMaterial = new THREE.MeshStandardMaterial({
+    color: 0xffff00,
+    emissive: 0x888800,
+    metalness: 0.5,
+    roughness: 0.1
+  });
+
+  // 应用高亮材质
+  building.material = highlightMaterial;
 };
 
+// 修改showBuildingInfo函数
+const showBuildingInfo = (info: any) => {
+  if (infoPanel.value) {
+    infoPanel.value.show(info);
+  }
+
+  // 添加临时高亮动画
+  if (info.highlightObject) {
+    const originalColor = info.highlightObject.material.color.getHex();
+    info.highlightObject.material.color.setHex(0xffff00);
+
+    setTimeout(() => {
+      info.highlightObject.material.color.setHex(originalColor);
+    }, 2000);
+  }
+};
 // 创建校园建筑物
-const createBuildings = async() => {
+const createBuildings = async () => {
   // 主教学楼
-  const mainBuilding = await createBuilding(15, 8, 10, 0xcccccc, 0, 4, 38,"教学楼");
+  const mainBuilding = await createBuilding(15, 8, 10, 0xcccccc, 0, 4, 38, "教学楼", {
+    name: "主教学楼",
+    function: "教学",
+    openTime: "07:00-22:00",
+    description: "校园主要教学场所,包含多个教室和实验室"
+  });
   scene.add(mainBuilding);
   // 添加左右两侧建筑
-  
-  const leftBuilding = await createBuilding(10, 5, 8, 0xcccccc, -30, 4, 36);
+
+  const leftBuilding = await createBuilding(10, 5, 8, 0xcccccc, -30, 3, 36, "一号教学楼");
   scene.add(leftBuilding);
-  const rightBuilding = await createBuilding(10, 5, 8, 0xcccccc, 30, 4, 36);
+  const rightBuilding = await createBuilding(10, 5, 8, 0xcccccc, 30, 3, 36, "二号教学楼");
   scene.add(rightBuilding);
 
   // 右侧三栋建筑
-  const rightBuilding1 = await createBuilding(10, 5, 8, 0xcccccc, 30, 4, 16);
+  const rightBuilding1 = await createBuilding(10, 5, 8, 0xcccccc, 30, 3, 16, "一号宿舍楼", {
+    name: "一号宿舍楼",
+    function: "休息",
+    openTime: "07:00-22:00",
+    description: "供学生休息"
+  });
   scene.add(rightBuilding1);
-  const rightBuilding2 = await createBuilding(10, 5, 8, 0xcccccc, 30, 4, -10);
+  const rightBuilding2 = await createBuilding(10, 5, 8, 0xcccccc, 30, 3, -4, "二号宿舍楼", {
+    name: "二号宿舍楼",
+    function: "休息",
+    openTime: "07:00-22:00",
+    description: "供学生休息"
+  });
   scene.add(rightBuilding2);
-  const rightBuilding3 = await createBuilding(10, 5, 8, 0xcccccc, 30, 4, -36);  
+  const rightBuilding3 = await createBuilding(10, 5, 8, 0xcccccc, 30, 3, -24, "三号宿舍楼", {
+    name: "三号宿舍楼",
+    function: "休息",
+    openTime: "07:00-22:00",
+    description: "供学生休息"
+  });
   scene.add(rightBuilding3);
   // 左侧三栋建筑
-  const leftBuilding1 = await createBuilding(10, 5, 8, 0xcccccc, -30, 4, 16);
-  scene.add(leftBuilding1);
-  const leftBuilding2 = await createBuilding(10, 5, 8, 0xcccccc, -30, 4, -10);
+  // const leftBuilding1 = await createBuilding(10, 5, 8, 0xcccccc, -30, 4, 16);
+  // scene.add(leftBuilding1);
+  const leftBuilding2 = await createBuilding(10, 5, 8, 0xcccccc, -0, 3, -35);
   scene.add(leftBuilding2);
-  const leftBuilding3 = await createBuilding(10, 5, 8, 0xcccccc, -30, 4, -36);  
+  const leftBuilding3 = await createBuilding(10, 5, 8, 0xcccccc, -30, 3, -35);
   scene.add(leftBuilding3);
   // 添加窗户细节
   addBuildingDetails();
 };
 
 // 创建建筑物通用函数
-const createBuilding = async(width: any, height: any, depth: any, color: any, x: any, y: any, z: any,name: any="") => {
+type BuildingInfo = {
+  name?: string;
+  function?: string;
+  openTime?: string;
+  [key: string]: any;
+};
+const createBuilding = async (
+  width: any,
+  height: any,
+  depth: any,
+  color: any,
+  x: any,
+  y: any,
+  z: any,
+  name: any = "",
+  buildingInfo: BuildingInfo = {}
+) => {
   const geometry = new THREE.BoxGeometry(width, height, depth);
   const material = new THREE.MeshStandardMaterial({
     color: color,
@@ -111,15 +226,29 @@ const createBuilding = async(width: any, height: any, depth: any, color: any, x:
   building.receiveShadow = true;
   // 添加建筑物名称
   if (name) {
+    building.name = name;
     const text = await create3DText(name, {
       size: 2,
       height: 0.1,
       color: 0xffffff,
-      position: { x: x, y: y + height/2 + 0.5, z: z },
-      rotation: { x: 0, y: 0, z: 0 }
+      position: { x: x, y: y + height / 2 + 0.5, z: z },
+      rotation: { x: 0, y: 0, z: 0 },
     });
     scene.add(text);
   }
+  // 添加可点击属性
+  building.userData = {
+    isBuilding: true,
+    buildingInfo: {
+      name: buildingInfo.name || "未命名建筑",
+      function: buildingInfo.function || "教学/办公",
+      openTime: buildingInfo.openTime || "08:00-22:00",
+      ...buildingInfo
+    }
+  };
+  clickableBuildings.push(building);
+  // console.log("添加可点击属性", building)
+
   return building;
 };
 
@@ -138,14 +267,14 @@ const create3DText = async (message: any, options: TextOptions = {}) => {
     height = 0.2,
     color = 0xffffff,
     position = { x: 0, y: 0, z: 0 },
-    rotation = { x: 0, y: 0, z: 0 }
+    rotation = { x: 0, y: 0, z: 0 },
   } = options;
-  
+
   // 加载字体
   const font = await new Promise<any>((resolve) => {
     fontLoader.load('/FangSong_Regular.json', (font) => resolve(font));
   });
-  
+
   // 创建文字几何体
   const textGeometry = new TextGeometry(message, {
     font: font,
@@ -154,20 +283,20 @@ const create3DText = async (message: any, options: TextOptions = {}) => {
     curveSegments: 12,
     bevelEnabled: false
   });
-  
+
   // 居中文字
   textGeometry.computeBoundingBox();
-  const textWidth = textGeometry.boundingBox &&textGeometry.boundingBox.max.x - textGeometry.boundingBox.min.x;
-  if(textWidth){
+  const textWidth = textGeometry.boundingBox && textGeometry.boundingBox.max.x - textGeometry.boundingBox.min.x;
+  if (textWidth) {
     textGeometry.translate(-textWidth / 2, 0, 0);
   }
-  
+
   const textMaterial = new THREE.MeshStandardMaterial({ color });
   const textMesh = new THREE.Mesh(textGeometry, textMaterial);
-  
+
   textMesh.position.set(position.x, position.y, position.z);
   textMesh.rotation.set(rotation.x, rotation.y, rotation.z);
-  
+
   return textMesh;
 };
 
@@ -325,6 +454,69 @@ const createEnvironment = () => {
   entranceRoadLine3.position.z = 25;
   scene.add(entranceRoadLine3);
 
+  // 右侧建筑物道路
+  const roadMaterial4 = new THREE.MeshStandardMaterial({ color: 0xdddddd });
+  const entranceRoad4 = new THREE.Mesh(
+    new THREE.PlaneGeometry(1.5, 90),
+    roadMaterial4
+  );
+  entranceRoad4.rotation.x = -Math.PI / 2;
+  entranceRoad4.position.y = 0.01;
+  entranceRoad4.position.z = 0;
+  entranceRoad4.position.x = 30.5;
+  scene.add(entranceRoad4);
+
+  const entranceRoad5 = new THREE.Mesh(
+    new THREE.PlaneGeometry(70, 1.5),
+    roadMaterial4
+  );
+  entranceRoad5.rotation.x = -Math.PI / 2;
+  entranceRoad5.position.y = 0.01;
+  entranceRoad5.position.z = 0;
+  entranceRoad5.position.x = -10;
+  entranceRoad5.position.z = -25;
+  scene.add(entranceRoad5);
+
+  const entranceRoad6 = new THREE.Mesh(
+    new THREE.PlaneGeometry(1.5, 30),
+    roadMaterial4
+  );
+  entranceRoad6.rotation.x = -Math.PI / 2;
+  entranceRoad6.position.y = 0.01;
+  entranceRoad6.position.z = 0;
+  entranceRoad6.position.z = -30;
+  scene.add(entranceRoad6);
+
+  const entranceRoad7 = new THREE.Mesh(
+    new THREE.PlaneGeometry(1.5, 10),
+    roadMaterial4
+  );
+  entranceRoad7.rotation.x = -Math.PI / 2;
+  entranceRoad7.position.y = 0.01;
+  entranceRoad7.position.z = -30;
+  entranceRoad7.position.x = -30;
+  scene.add(entranceRoad7);
+
+  const entranceRoad8 = new THREE.Mesh(
+    new THREE.PlaneGeometry(10, 1.5),
+    roadMaterial4
+  );
+  entranceRoad8.rotation.x = -Math.PI / 2;
+  entranceRoad8.position.y = 0.01;
+  entranceRoad8.position.z = 35;
+  entranceRoad8.position.x = 40;
+  scene.add(entranceRoad8);
+
+  const entranceRoad9 = new THREE.Mesh(
+    new THREE.PlaneGeometry(10, 1.5),
+    roadMaterial4
+  );
+  entranceRoad9.rotation.x = -Math.PI / 2;
+  entranceRoad9.position.y = 0.01;
+  entranceRoad9.position.z = 35;
+  entranceRoad9.position.x = -40;
+  scene.add(entranceRoad9);
+
   // 添加树木
   // for (let i = -40; i <= 40; i += 10) {
   //   for (let j = -40; j <= 40; j += 10) {
@@ -362,14 +554,28 @@ const createTree = (x: any, y: any, z: any) => {
   scene.add(leaves);
 };
 // 创建椭圆操场
-const createEllipticalPlayground = (x:any, y:any, z:any) => {
+const createEllipticalPlayground = (x: any, y: any, z: any) => {
+
+  // const PlaneGeometry  = new THREE.PlaneGeometry(25, 35);
+  // const PlaneMaterial = new THREE.MeshStandardMaterial({ 
+  //   color: 0xdddddd,
+  //   transparent:true,//开启透明
+  //   opacity:0.5,//设置透明度
+  //   // side: THREE.DoubleSide
+  // });
+  // const Plane = new THREE.Mesh(PlaneGeometry, PlaneMaterial);
+  // Plane.rotation.x = -Math.PI / 2;
+  // Plane.position.set(x, y + 0.01, z);
+  // Plane.receiveShadow = true;
+  // scene.add(Plane);
+
   // 椭圆跑道参数
   const a = 13; // 长轴半径
   const b = 18; // 短轴半径
   const trackWidth = 3; // 跑道宽度
   const innerA = a - trackWidth; // 内椭圆长轴
   const innerB = b - trackWidth; // 内椭圆短轴
-  
+
   // 创建椭圆跑道
   const trackGeometry = new THREE.EllipseCurve(
     0, 0,            // 中心点
@@ -378,21 +584,21 @@ const createEllipticalPlayground = (x:any, y:any, z:any) => {
     false,           // 是否顺时针
     0                // 旋转角度
   );
-  
+
   const innerTrackGeometry = new THREE.EllipseCurve(
     0, 0,
     innerA, innerB,
     0, 2 * Math.PI,
     false,
-    0
+    0,
   );
-  
+
   const trackPoints = trackGeometry.getPoints(100);
   const innerTrackPoints = innerTrackGeometry.getPoints(100);
-  
+
   // 合并点集形成跑道形状
   const allPoints = trackPoints.concat(innerTrackPoints.reverse());
-  
+
   // 创建跑道形状
   const trackShape = new THREE.Shape();
   trackShape.moveTo(trackPoints[0].x, trackPoints[0].y);
@@ -404,16 +610,16 @@ const createEllipticalPlayground = (x:any, y:any, z:any) => {
     trackShape.lineTo(innerTrackPoints[i].x, innerTrackPoints[i].y);
   }
   trackShape.lineTo(trackPoints[0].x, trackPoints[0].y);
-  
+
   // 创建跑道网格
   const trackExtrudeSettings = {
     steps: 1,
     depth: 0.1,
     bevelEnabled: false
   };
-  
+
   const trackGeometry3D = new THREE.ExtrudeGeometry(trackShape, trackExtrudeSettings);
-  const trackMaterial = new THREE.MeshStandardMaterial({ 
+  const trackMaterial = new THREE.MeshStandardMaterial({
     color: 0x654321,
     roughness: 0.9
   });
@@ -421,23 +627,23 @@ const createEllipticalPlayground = (x:any, y:any, z:any) => {
   track.rotation.x = -Math.PI / 2;
   track.position.set(x, y, z);
   scene.add(track);
-  
+
   // 创建内部足球场
   // 使用CircleGeometry并通过缩放实现椭圆
   const fieldGeometry = new THREE.CircleGeometry(1, 100);
-  const fieldMaterial = new THREE.MeshStandardMaterial({ 
+  const fieldMaterial = new THREE.MeshStandardMaterial({
     color: 0x228B22,
-    side: THREE.DoubleSide
+    // side: THREE.DoubleSide
   });
   const field = new THREE.Mesh(fieldGeometry, fieldMaterial);
   field.scale.set(innerA, innerB, 1); // 椭圆缩放
   field.rotation.x = -Math.PI / 2;
   field.position.set(x, y + 0.01, z);
   scene.add(field);
-  
+
   // 添加足球场标记线
   const lineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
-  
+
   // 中圈
   const centerCircleGeometry = new THREE.CircleGeometry(1, 32);
   const centerCircle = new THREE.Line(
@@ -448,7 +654,7 @@ const createEllipticalPlayground = (x:any, y:any, z:any) => {
   centerCircle.rotation.x = -Math.PI / 2;
   centerCircle.position.set(x, y + 0.02, z);
   scene.add(centerCircle);
-  
+
   // 中线
   const points = [];
   points.push(new THREE.Vector3(-innerA, 0, 0));
@@ -458,9 +664,166 @@ const createEllipticalPlayground = (x:any, y:any, z:any) => {
   midline.rotation.x = -Math.PI / 2;
   midline.position.set(x, y + 0.02, z);
   scene.add(midline);
-  
+
+
+
   return { track, field };
 };
+// 创建篮球场(尺寸调整为20x10)
+const createBasketballCourt = (x: any, y: any, z: any, rotationY = 0) => {
+  // 篮球场尺寸 (调整为20x10)
+  const courtLength = 20;
+  const courtWidth = 10;
+  const lineWidth = 0.2;
+
+  // 创建篮球场地板
+  const courtGeometry = new THREE.PlaneGeometry(courtLength, courtWidth);
+  const courtMaterial = new THREE.MeshStandardMaterial({
+    color: 0x1a3b6e, // 深蓝色
+    roughness: 0.7,
+    metalness: 0.1
+  });
+  const court = new THREE.Mesh(courtGeometry, courtMaterial);
+  court.rotation.x = -Math.PI / 2;
+  court.position.set(x, y, z);
+  court.rotation.y = rotationY;
+  court.receiveShadow = true;
+  scene.add(court);
+
+  // 篮球场边界线
+  const borderLineGeometry = new THREE.EdgesGeometry(new THREE.BoxGeometry(courtLength, 0.1, courtWidth));
+  const borderLineMaterial = new THREE.LineBasicMaterial({ color: 0xffffff, linewidth: 2 });
+  const borderLine = new THREE.LineSegments(borderLineGeometry, borderLineMaterial);
+  borderLine.position.set(x, y + 0.01, z);
+  borderLine.rotation.y = rotationY;
+  scene.add(borderLine);
+
+  // 中线
+  const midLineGeometry = new THREE.BufferGeometry().setFromPoints([
+    new THREE.Vector3(0, 0.02, -courtWidth / 2),
+    new THREE.Vector3(0, 0.02, courtWidth / 2)
+  ]);
+  const midLine = new THREE.Line(midLineGeometry, borderLineMaterial);
+  midLine.position.set(x, y + 0.02, z);
+  midLine.rotation.y = rotationY;
+  scene.add(midLine);
+
+  // 中圈 (按比例缩小)
+  const centerCircleGeometry = new THREE.RingGeometry(1.2, 1.4, 32);
+  const centerCircleMaterial = new THREE.MeshBasicMaterial({
+    color: 0xffffff,
+    side: THREE.DoubleSide
+  });
+  const centerCircle = new THREE.Mesh(centerCircleGeometry, centerCircleMaterial);
+  centerCircle.rotation.x = -Math.PI / 2;
+  centerCircle.position.set(x, y + 0.02, z);
+  centerCircle.rotation.y = rotationY;
+  scene.add(centerCircle);
+
+  // 三分线 (按比例调整)
+  const threePointLineGeometry = new THREE.EllipseCurve(
+    0, 0,
+    5.7, 5.7, // 缩小三分线半径
+    -Math.PI * 0.4, Math.PI * 0.4,
+    false,
+    0
+  );
+  const threePointPoints = threePointLineGeometry.getPoints(50);
+  const threePointLine = new THREE.Line(
+    new THREE.BufferGeometry().setFromPoints(threePointPoints),
+    borderLineMaterial
+  );
+  threePointLine.position.set(x - courtLength / 2 + 1.2, y + 0.02, z); // 调整位置
+  threePointLine.rotation.x = Math.PI / 2;
+  threePointLine.rotation.y = rotationY;
+  scene.add(threePointLine);
+  // 三分线 (按比例调整)
+  const threePointLineGeometry1 = new THREE.EllipseCurve(
+    0, 0,
+    -5.7, 5.7, // 缩小三分线半径
+    -Math.PI * 0.4, Math.PI * 0.4,
+    false,
+    0
+  );
+  const threePointPoints1 = threePointLineGeometry1.getPoints(50);
+  const threePointLine1 = new THREE.Line(
+    new THREE.BufferGeometry().setFromPoints(threePointPoints1),
+    borderLineMaterial
+  );
+  threePointLine1.position.set(x + courtLength / 2 - 1.2, y + 0.02, z); // 调整位置
+  threePointLine1.rotation.x = Math.PI / 2;
+  threePointLine1.rotation.y = rotationY;
+  scene.add(threePointLine1);
+
+  // 篮板和篮筐
+  const createBackboard = (offsetX: any) => {
+    // 篮板 (尺寸也相应缩小)
+    const backboardGeometry = new THREE.BoxGeometry(1.5, 1, 0.05);
+    const backboardMaterial = new THREE.MeshStandardMaterial({
+      color: 0xffffff,
+      transparent: true,
+      opacity: 0.3,
+      metalness: 0.9,
+      roughness: 0.1
+    });
+    const backboard = new THREE.Mesh(backboardGeometry, backboardMaterial);
+    backboard.position.set(
+      x + offsetX * (courtLength / 2 - 0.4), // 调整位置
+      y + 2.5, // 降低高度
+      z
+    );
+    backboard.rotation.y = rotationY + (offsetX > 0 ? Math.PI : 0);
+    scene.add(backboard);
+
+    // 篮筐支架 (缩短)
+    const poleGeometry = new THREE.CylinderGeometry(0.05, 0.05, 2.5, 16);
+    const poleMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc });
+    const pole = new THREE.Mesh(poleGeometry, poleMaterial);
+    pole.position.set(
+      x + offsetX * (courtLength / 2 - 0.4),
+      y + 1.25,
+      z
+    );
+    scene.add(pole);
+
+    // 篮筐 (环)
+    const hoopGeometry = new THREE.TorusGeometry(0.2, 0.02, 16, 32);
+    const hoopMaterial = new THREE.MeshStandardMaterial({ color: 0xff7700 });
+    const hoop = new THREE.Mesh(hoopGeometry, hoopMaterial);
+    hoop.position.set(
+      x + offsetX * (courtLength / 2 - 0.4),
+      y + 2.55,
+      z
+    );
+    hoop.rotation.x = Math.PI / 2;
+    hoop.rotation.y = rotationY + (offsetX > 0 ? Math.PI : 0);
+    scene.add(hoop);
+
+    // 篮网 (缩小)
+    const netGeometry = new THREE.ConeGeometry(0.2, 0.35, 8);
+    const netMaterial = new THREE.MeshStandardMaterial({
+      color: 0xffffff,
+      wireframe: true,
+      transparent: true,
+      opacity: 0.7
+    });
+    const net = new THREE.Mesh(netGeometry, netMaterial);
+    net.position.set(
+      x + offsetX * (courtLength / 2 - 0.4),
+      y + 2.4,
+      z
+    );
+    net.rotation.x = Math.PI;
+    net.rotation.y = rotationY + (offsetX > 0 ? Math.PI : 0);
+    scene.add(net);
+  };
+
+  // 创建两个篮板
+  createBackboard(1);  // 正方向篮板
+  createBackboard(-1); // 反方向篮板
+
+  return court;
+};
 
 // 动画循环
 const animate = () => {
@@ -471,19 +834,75 @@ const animate = () => {
 
 // 处理窗口大小变化
 const onWindowResize = () => {
-  camera.aspect = width / height;
+  camera.aspect = 3 / 2;
   camera.updateProjectionMatrix();
-  renderer.setSize(width, height);
+
+  let containerWidth = width;
+  let containerHeight = height;
+  if (container.value) {
+    containerWidth = container.value.clientWidth || width;
+    containerHeight = container.value.clientHeight || height;
+  }
+  renderer.setSize(containerWidth, containerWidth * (2 / 3));
+};
+// 添加鼠标移动事件处理
+const onDocumentMouseMove = (event: any) => {
+  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
+  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
+
+  raycaster.setFromCamera(mouse, camera);
+  const intersects = raycaster.intersectObjects(scene.children, true);
+
+  // 重置所有建筑物的悬停状态
+  scene.traverse((obj: any) => {
+    if (obj.userData.isBuilding && obj.userData.originalColor) {
+      obj.material.color.setHex(obj.userData.originalColor);
+    }
+  });
+
+  if (intersects.length > 0) {
+    const hoveredObject = intersects[0].object;
+    if (hoveredObject.userData.isBuilding) {
+      // 存储原始颜色
+      if (!hoveredObject.userData.originalColor) {
+        hoveredObject.userData.originalColor = hoveredObject.material.color.getHex();
+      }
+      // 设置悬停颜色
+      hoveredObject.material.color.setHex(0xaaaaaa);
+
+      // 更改鼠标指针
+      if (container.value) {
+        container.value.style.cursor = 'pointer';
+      }
+      return;
+    }
+  }
+  if (container.value) {
+    container.value.style.cursor = 'default';
+  }
 };
 
 onMounted(() => {
-  initScene();
-  animate();
+
+  // 获取容器宽高
+  let containerWidth = width;
+  let containerHeight = height;
+  if (container.value) {
+    containerWidth = container.value.clientWidth || width;
+    containerHeight = container.value.clientHeight || height;
+    initScene(containerWidth, containerHeight);
+    animate();
+  }
+
   window.addEventListener('resize', onWindowResize);
+  window.addEventListener('click', handleClick, false);
+  window.addEventListener('mousemove', onDocumentMouseMove, false);
 });
 
 onBeforeUnmount(() => {
   window.removeEventListener('resize', onWindowResize);
+  window.removeEventListener('mousemove', onDocumentMouseMove, false);
+  window.removeEventListener('click', handleClick, false);
   cancelAnimationFrame(animateId);
   if (container.value && container.value.contains(renderer.domElement)) {
     container.value.removeChild(renderer.domElement);
@@ -492,7 +911,9 @@ onBeforeUnmount(() => {
 </script>
 
 <template>
-  <div ref="container" id="webglContainer" style=""></div>
+  <div ref="container" id="webglContainer" style="width: 100%; height: 100%;">
+    <BuildingInfoPanel ref="infoPanel" />
+  </div>
 </template>
 
 <style scoped lang="less">

+ 61 - 0
src/pages/Entry/components/RealtimeData/codeMirrow.vue

@@ -0,0 +1,61 @@
+<template>
+    <div ref="editorRef" class="editor-main"></div>
+</template>
+<script lang="ts" setup>
+import { basicSetup, EditorView } from "codemirror";
+import { EditorState } from "@codemirror/state";
+import { python } from '@codemirror/lang-python'
+import { onMounted, ref,watch } from "vue";
+const props = defineProps<{
+    code: string;
+}>();
+
+const editorRef = ref();
+const editorView = ref();
+const initEditor = () => {
+    if (editorView.value) {
+        editorView.value.destroy();
+    }
+    const startState = EditorState.create({
+        doc: props.code,
+        extensions: [basicSetup, python()],
+    });
+    if (editorRef.value) {
+        editorView.value = new EditorView({
+            state: startState,
+            parent: editorRef.value,
+        });
+    }
+};
+
+onMounted(() => {
+    initEditor();
+});
+
+// 监听 props.code 变化,实时更新编辑器内容
+watch(
+    () => props.code,
+    (newValue) => {
+        if (editorView.value) {
+            // 只替换文档内容,不重建整个编辑器
+            editorView.value.dispatch({
+                changes: {
+                    from: 0,
+                    to: editorView.value.state.doc.length,
+                    insert: newValue,
+                },
+            });
+        } else {
+            initEditor();
+        }
+    }
+);
+
+</script>
+
+<style  scoped>
+.editor-main {
+   max-height: 500px;
+   overflow-y: auto;
+}
+</style>

+ 169 - 18
src/pages/Entry/components/RealtimeData/index.vue

@@ -1,36 +1,167 @@
 <script setup lang="ts">
-import { ref } from "vue";
+import { ref, onMounted } from "vue";
 import ClipBox from "@components/ClipBox/index.vue";
+import useMultipleWebSockets from "../../../../service/socket/socket.ts"
+import useSockets from "../../../../hooks/useSockets.ts";
 
-const tableData = ref([]);
+
+const { sockets, connectionStatus, connected } = useSockets();
+declare global {
+  interface Window {
+    tableData?: any;
+    socketUrl?: any;
+  }
+}
+
+const tableData = ref<{ name: string; ip: string, status: string }[]>([]);
 const addCocoPiVisible = ref(false);
+const editCocoPiVisible = ref(false);
 const CocoPiData = ref<any>({});
+const socketUrl = ref<string[]>([])
+const problem = ref("")
+const codeRef = ref<any>(null)
 
+onMounted(async () => {
+  getData()
+})
 
-const onSubmit = () => {
-  console.log("提交数据", CocoPiData.value);
+const getData = async () => {
+  tableData.value = JSON.parse(localStorage.getItem('tableData') || '[]').map((item: any) => {
+    item.status = "disConnected"
+    item.socketData = "无数据"
+    return item
+  })
+  if (tableData.value.length > 0) {
+    socketUrl.value = JSON.parse(localStorage.getItem('socketUrl') || '[]')
+    initConnections(socketUrl.value)
+  }
+};
+const initConnections = async (urls: any) => {
+  await Promise.all(urls.map((url: any) => connected(url).then(res => {
+    (res as WebSocket).onmessage = (e: any) => {
+      tableData.value.map((i: any) => {
+        if (`wss://${i.ip}:5678/` === `${(res as WebSocket).url}`) {
+          i.socketData = e.data;
+          i.status = 'connected';
+        }
+      })
+    };
+  }).catch(err => {
+    tableData.value.map((i: any) => {
+      if (`wss://${i.ip}:5678/` === err.currentTarget.url) {
+        i.socketData = "无数据";
+        i.status = 'disConnected';
+      }
+    })
+  })));
+};
+const onSubmit = async () => {
   if (!CocoPiData.value.name || !CocoPiData.value.ip) {
     alert("请填写完整信息");
     return;
+  } else if (socketUrl.value.includes(`wss://${CocoPiData.value.ip}:5678`)) {
+    alert("该设备IP已添加,请勿重复添加");
+    return;
+  }
+
+  const { sockets, send, sendData, status } = await useMultipleWebSockets([`wss://${CocoPiData.value.ip}:5678`])
+  setTimeout(() => {
+    if (status[`wss://${CocoPiData.value.ip}:5678`] === "connected") {
+      let obj = {
+        name: CocoPiData.value.name,
+        ip: CocoPiData.value.ip,
+        status: "connected",
+        socketData: sendData[`wss://${CocoPiData.value.ip}:5678`],
+        send: send
+      }
+      tableData.value.push(obj)
+      socketUrl.value.push(`wss://${CocoPiData.value.ip}:5678`)
+      addCocoPiVisible.value = false
+      localStorage.setItem("tableData", JSON.stringify(tableData.value))
+      localStorage.setItem("socketUrl", JSON.stringify(socketUrl.value))
+      CocoPiData.value = {}
+    } else {
+      alert("请检查IP是否正确或者网络是否正常")
+    }
+  }, 1000)
+}
+
+const detectionIP = () => {
+  console.log("检测ip", CocoPiData.value.ip);
+  if (!CocoPiData.value.ip) {
+    alert("请填写ip");
+    return;
   }
+  window.open(`https://${CocoPiData.value.ip}:5678/`);
+}
+
+const handleEdit = (data: any) => {
+  editCocoPiVisible.value = true
+  console.log("handleEdit", data);
 }
 
+const handleDelete = (data: any) => {
+  console.log("handleDelete", data);
+}
+
+
+function insert_operating_record(rawData: string) {
+  // 事件流数据通常以 "data: " 开头,以 "\n\n" 分隔事件
+  const events = rawData.split('\n\n').filter(Boolean);
+
+  events.forEach(event => {
+    // 移除 "data: " 前缀并解析JSON(如果是JSON数据)
+    if (event.startsWith('data: ')) {
+      const data = event.substring(6).trim();
+      try {
+        const parsedData = JSON.parse(data);
+        console.log('Parsed event:', parsedData);
+        // 在这里处理解析后的数据
+      } catch (e) {
+        console.log('Raw event data:', data);
+      }
+    }
+  });
+}
+
+const copyCode = () => {
+  // sockets.value['wss://192.168.232.46:5678'].send(`echo close > /root/socket.txt`)
+  
+  let data = problem.value
+  console.log(data)
+  sockets.value['wss://192.168.232.46:5678'].send(`echo ${data} > /root/socket.txt`)
+};
+
+const clearAlldevice = () => { 
+  tableData.value = []
+  localStorage.setItem("tableData", JSON.stringify([]))
+  localStorage.setItem("socketUrl", JSON.stringify([]))
+};
 </script>
 
 <template>
   <ClipBox title="硬件信息">
-    <div data-title="PV" class="realtime-content">
-      <el-table :data="tableData">
-        <el-table-column label="序号" width="80">
+    <div class="realtime-content">
+      <div style="text-align: right;padding:0 0 10px 0;">
+        <el-button @click="clearAlldevice" type="danger">清除设备</el-button>
+        <el-button @click="addCocoPiVisible = true" type="primary">添加硬件</el-button>
+      </div>
+      <el-table :data="tableData" style="width: 100%;">
+        <!-- <el-table-column label="序号" width="80" props="index">
+        </el-table-column> -->
+        <el-table-column label="名字" prop="name" width="80">
         </el-table-column>
-        <el-table-column label="数据项" prop="item" width="200">
-          <template #default="scope">
-            <span>{{ scope.row.item }}</span>
-          </template>
+        <el-table-column label="IP" prop="ip" width="130">
+        </el-table-column>
+        <el-table-column label="连接状态" prop="status" width="120">
+        </el-table-column>
+        <el-table-column label="数据" prop="socketData" >
+          
         </el-table-column>
-        <el-table-column label="数值" prop="value" width="200">
-          <template #default="scope">
-            <span>{{ scope.row.value }}</span>
+        <el-table-column label="操作" width="140">
+          <template v-slot="scope">
+            <el-button type="primary" size="small" @click="handleEdit(scope.row)">操作</el-button>
+            <el-button type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -41,7 +172,8 @@ const onSubmit = () => {
           <el-input v-model="CocoPiData.name" placeholder="请输入硬件名称"></el-input>
         </el-form-item>
         <el-form-item label="硬件ip:">
-          <el-input v-model="CocoPiData.ip" placeholder="请输入硬件IP地址"></el-input>
+          <el-input v-model="CocoPiData.ip" placeholder="请输入硬件IP地址" style="width: 200px;"></el-input>
+          <el-button @click="detectionIP">检测ip 服务是否启动</el-button>
         </el-form-item>
         <!-- <el-form-item label="地图位置:">
           <el-input v-model="CocoPiData.port" placeholder="请输入硬件端口" type="number"></el-input>
@@ -52,6 +184,17 @@ const onSubmit = () => {
         </el-form-item>
       </el-form>
     </el-dialog>
+    <el-dialog v-model="editCocoPiVisible" title="操作" width="600">
+      <!-- <el-select>
+        <el-option label="" value=""></el-option>
+        <el-option label="" value=""></el-option>
+      </el-select> -->
+      <el-input style="width: 60%;" v-model="problem" aria-placeholder="请输入功能" placeholder="请输入功能"></el-input>
+      <!-- <el-button @click="onCodeSubmit">发送指令</el-button> -->
+      <br /><br />
+      <!-- <span>内容显示:</span> -->
+        <el-button @click="copyCode">发送指令</el-button>
+    </el-dialog>
   </ClipBox>
 </template>
 
@@ -63,16 +206,16 @@ const onSubmit = () => {
   display: flex;
   justify-content: space-between;
   z-index: 3;
+  width: 100%;
 }
 
 .realtime-content {
+  width: 100%;
   flex: 1;
   background: #fff;
   border-radius: 15px;
-  padding: 25px;
-  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
+  padding: 15px;
   backdrop-filter: blur(10px);
-  border: 1px solid rgba(255, 255, 255, 0.1);
   transition: all 0.3s ease;
 
   ul {
@@ -119,4 +262,12 @@ const onSubmit = () => {
     margin-left: 20 * @px2vw;
   }
 }
+
+.code-show {
+  max-height: 500px;
+  overflow-y: auto;
+  min-height: 100px;
+  border: 1px solid #ddd;
+  border-radius: 5px;
+}
 </style>

+ 453 - 0
src/pages/Entry/components/homeRight/right.vue

@@ -0,0 +1,453 @@
+<template>
+    <div class="right">
+        <!-- 智能助手侧边栏 -->
+        <div class="assistant-sidebar">
+            <div class="assistant-header">
+                <!-- <div class="assistant-avatar">
+                    <i class="fas fa-robot"></i>
+                </div> -->
+                <div class="assistant-title">
+                    <h2>教学助手</h2>
+                    <p>随时为您提供帮助</p>
+                </div>
+            </div>
+
+            <div class="assistant-chat" ref="codeRef">
+                <!-- <div class="message assistant-message">
+                    您好!我是AI教学助手,有什么可以帮您的吗?
+                </div>
+
+                <div class="message user-message">
+                    如何启动沙盘的交通模拟?
+                </div>
+
+                <div class="message assistant-message">
+                    您可以在沙盘控制面板点击"交通模拟"按钮。系统会自动启动预设的交通场景,包括车辆运行、信号灯控制等。
+                </div> -->
+                <div v-if="datas.length" style="display: flex;flex-direction:column;gap: 20px;" v-for="item in datas"
+                    :key="item.problem">
+                    <div class="message user-message">
+                        {{ item.problem }}
+                    </div>
+                    <div class="message assistant-message">
+                        <span v-if="item.codeData" v-html="item.codeData"></span>
+                        <svg v-else width="50" height="50" viewBox="0 0 50 50" fill="none"
+                            xmlns="http://www.w3.org/2000/svg" style="margin: 0 auto;">
+                            <circle cx="25" cy="25" r="20" fill="none" stroke="#00d2ff" stroke-width="4"
+                                stroke-linecap="round" stroke-dasharray="31.4 31.4" transform="rotate(-90 25 25)">
+                                <animateTransform attributeName="transform" type="rotate" from="0 25 25" to="360 25 25"
+                                    dur="1s" repeatCount="indefinite" />
+                            </circle>
+                        </svg>
+                    </div>
+                </div>
+            </div>
+
+            <div class="chat-input">
+                <textarea v-model="problem" type="text" placeholder="输入您的问题..." @keyup.enter="onCodeSubmit" ></textarea>
+                <button @click="onCodeSubmit">
+                    <svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 32 32" fill="none">
+                        <rect width="32" height="32" rx="16" fill="#68A1FD" />
+                        <path
+                            d="M13.188 23.2586C12.9758 23.2586 12.7723 23.1954 12.6223 23.0829C12.4723 22.9703 12.388 22.8177 12.388 22.6586V17.1806C12.388 17.0006 12.496 16.8296 12.684 16.7156L19.164 12.7766C19.3289 12.6804 19.5376 12.6365 19.7452 12.6543C19.9527 12.6721 20.1428 12.7502 20.2746 12.8718C20.4064 12.9934 20.4694 13.1488 20.4503 13.3049C20.4311 13.4609 20.3312 13.6051 20.172 13.7066L13.988 17.4656V20.8556L15.932 18.9116C16.168 18.6746 16.604 18.6026 16.952 18.7436L20.176 20.0396L24 9.74359C24.076 9.54259 23.92 9.41659 23.848 9.37159C23.776 9.32659 23.588 9.23359 23.336 9.32059L8.83598 14.4236L10.716 15.2336C11.1 15.3986 11.232 15.7646 11.012 16.0526C10.792 16.3406 10.304 16.4396 9.91998 16.2746L6.70798 14.8916C6.58077 14.837 6.47612 14.7571 6.40555 14.6608C6.33497 14.5645 6.30121 14.4554 6.30798 14.3456C6.31998 14.1206 6.49598 13.9226 6.76798 13.8266L22.656 8.23459C23.4 7.97359 24.252 8.05759 24.88 8.45659C25.1856 8.64798 25.4107 8.90178 25.5279 9.1872C25.645 9.47263 25.6492 9.77741 25.54 10.0646L21.444 21.0836C21.4117 21.1701 21.3539 21.2501 21.2748 21.3176C21.1958 21.385 21.0975 21.4383 20.9872 21.4735C20.877 21.5086 20.7576 21.5247 20.638 21.5206C20.5183 21.5165 20.4014 21.4922 20.296 21.4496L16.8 20.0426L13.828 23.0186C13.672 23.1716 13.432 23.2586 13.188 23.2586Z"
+                            fill="white" />
+                    </svg>
+                </button>
+            </div>
+        </div>
+        <!-- <div class="chat-container">
+            <div class="chat-messages">
+                <div v-if="datas.length" class="chat-message ai" v-for="item in datas" :key="item.problem">
+                    <div class="chat-message user" style="text-align: right;">
+                        {{ item.problem }}<span>:你</span>
+                    </div>
+                    <div class="chat-message ai">
+                        <span>AI:</span>
+                        <span v-html="item.codeData"></span>
+                    </div>
+                </div>
+
+            </div>
+            <el-input v-model="problem" placeholder="请输入你的问题..." @keyup.enter="onCodeSubmit" clearable
+                class="chat-input" />
+        </div>
+        <br /><br />
+        <div class="code-show" v-html="codeData" ref="codeRef"></div>
+        <el-button type="primary" @click="onCodeSubmit">提交</el-button> -->
+    </div>
+</template>
+<script setup lang="ts">
+import { ref, onMounted, watch } from 'vue';
+import { v4 as uuidv4 } from "uuid";
+import { fetchEventSource } from '@microsoft/fetch-event-source';
+import MarkdownIt from 'markdown-it';
+import useSockets from "../../../../hooks/useSockets.ts";
+import sendBtnImg from '../../../../assets/images/send_btn.svg'
+
+
+const { sockets, connectionStatus, connected } = useSockets();
+const problem = ref('')
+const codeData = ref('')
+const generatedUuid = ref('')
+const codeRef = ref<any>(null)
+const datas = ref<{ problem: string, codeData: string }[]>([])
+const userId = ref('')
+
+onMounted(() => {
+    userId.value = localStorage.getItem('userId') || uuidv4()
+    getAiAgentChat(userId.value)
+
+})
+
+const getAiAgentChat = async (userId: any) => {
+    fetch('https://gpt4.cocorobo.cn/get_agent_chat', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+            "userid": "ce601631-50e4-11ed-8c78-005056b86db5",
+            "groupid": "d2e3c73a-7d8e-4da9-a95e-c6207782bab7"
+        }),
+
+    }).then((res: any) => res.json()).then((data: any) => {
+        datas.value = JSON.parse(data.FunctionResponse).map((r: any) => {
+            return {
+                problem: decodeURIComponent(r.problem),
+                codeData: decodeURIComponent(r.answer)
+            }
+        })
+
+    }).catch((error) => {
+        console.error('Error:', error);
+    });
+}
+
+const onCodeSubmit = async () => {
+    generatedUuid.value = uuidv4()
+    localStorage.setItem('userId', userId.value)
+    codeData.value = "123123";
+    let datasLenth = datas.value.length
+    const problemContent = problem.value
+    datas.value[datasLenth] = { problem: problemContent, codeData: "" }
+    let params = {
+        "id": "d2e3c73a-7d8e-4da9-a95e-c6207782bab7",
+        "message": problem.value,
+        "userId": "ce601631-50e4-11ed-8c78-005056b86db5",
+        "model": "open-gpt-4.1-mini",
+        "file_ids": [],
+        "sound_url": "",
+        "temperature": 0.2,
+        "top_p": 1,
+        "max_completion_tokens": 4096,
+        "stream": true,
+        "uid": generatedUuid.value,
+    }
+    
+    let alltext = ''
+    let newalltext = '';
+    problem.value = ''
+    const md = new MarkdownIt({
+        html: true,
+    });
+    const abortController = new AbortController();
+    await fetchEventSource('https://appapi.cocorobo.cn/api/agentchats/ai_agent_chat', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(params),
+        signal: abortController.signal,
+        onmessage: (msg) => {
+            try {
+                const data = JSON.parse(msg.data);
+                if (data.content == '[DONE]') {
+                    const finalText = md.render(alltext)
+                    console.log(finalText)
+                    codeData.value = finalText
+                    console.log(datas.value[datasLenth])
+                    const insertData = {
+                        userId: "ce601631-50e4-11ed-8c78-005056b86db5",
+                        groupId: "d2e3c73a-7d8e-4da9-a95e-c6207782bab7",
+                        type: "chat",
+                        problem: encodeURIComponent(datas.value[datasLenth].problem),
+                        answer: encodeURIComponent(finalText),
+                        userName: "jidechao@cocorobo.cc",
+                        file_id: "",
+                        alltext,
+                    }
+                    insert_operating_record(insertData)
+                    return
+                }
+                if (data.content) {
+                    alltext += data.content;
+                    newalltext = alltext;
+                    newalltext = newalltext.replace(/\\n/g, '\n');
+                    newalltext = newalltext.replace(/\\/g, '');
+
+                    // 处理代码块
+                    if (alltext.split('```').length % 2 === 0) {
+                        newalltext += '\n```\n';
+                    }
+                    // 渲染 markdown
+                    newalltext = md.render(newalltext);
+                    codeRef.value.scrollTop = codeRef.value.scrollHeight;
+                    datas.value[datasLenth] = { problem: problemContent, codeData: newalltext }
+                }
+            } catch (e) {
+                console.log(e)
+            }
+        },
+        onclose() {
+            console.log("close")
+        },
+        onerror(error) {
+            console.log(error)
+        },
+    })
+
+}
+const insert_operating_record = async (data: any) => {
+    fetch('https://gpt4.cocorobo.cn/insert_chat', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json',
+        },
+        body: JSON.stringify(data),
+
+    }).then((res: any) => res.json()).then((data: any) => {
+        console.log(data)
+    }).catch((error) => {
+        console.error('Error:', error);
+    })
+}
+
+
+watch(() => datas.value, (newValue, oldValue) => {
+    if (newValue) {
+        setTimeout(() => {
+            console.log(document.getElementsByClassName("assistant-chat")[0].scrollHeight);
+            codeRef.value.scrollTop = codeRef.value.scrollHeight;
+        }, 1000);
+    }
+
+})
+
+</script>
+<style>
+.chat-messages {
+    border: 1px solid #ccc;
+    border-radius: 8px;
+    margin: 15px auto;
+}
+
+.chat-message {
+    padding: 5px;
+}
+
+/* 智能助手侧边栏 */
+.assistant-sidebar {
+    width: 100%;
+    /* background: #f2f3f5; */
+    /* border-left: 1px solid rgba(0, 210, 255, 0.2); */
+    display: flex;
+    flex-direction: column;
+    /* box-shadow: -5px 0 15px rgba(0, 0, 0, 0.3); */
+    height: 100%;
+    overflow-y: auto;
+    border-radius: 10px;
+    color: #fff;
+}
+
+.assistant-header {
+    padding: 20px;
+    background: rgba(0, 0, 0, 0.2);
+    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+    display: flex;
+    align-items: center;
+    gap: 15px;
+}
+
+.assistant-avatar {
+    width: 50px;
+    height: 50px;
+    background: linear-gradient(135deg, #00d2ff, #3a7bd5);
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 1.5rem;
+}
+
+.assistant-title h2 {
+    font-size: 1.5rem;
+    margin-bottom: 5px;
+}
+
+.assistant-title p {
+    opacity: 0.7;
+    font-size: 0.9rem;
+}
+
+.assistant-chat {
+    flex: 1;
+    padding: 20px;
+    overflow-y: auto;
+    overflow-x: hidden;
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+    min-height: calc(100% - 200px);
+}
+
+.message {
+    padding: 15px;
+    border-radius: 15px;
+    position: relative;
+    animation: fadeIn 0.3s ease-out;
+    color: #fff;
+}
+
+.message code {
+    white-space: pre-wrap;
+}
+
+.user-message {
+    background: rgba(0, 210, 255, 0.15);
+    align-self: flex-end;
+    border-bottom-right-radius: 5px;
+}
+
+.assistant-message {
+    background: rgba(255, 255, 255, 0.1);
+    align-self: flex-start;
+    border-bottom-left-radius: 5px;
+}
+
+.suggested-questions {
+    padding: 15px;
+    border-top: 1px solid rgba(255, 255, 255, 0.1);
+    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+    background: rgba(0, 0, 0, 0.1);
+}
+
+.suggested-title {
+    margin-bottom: 10px;
+    font-size: 1.1rem;
+    display: flex;
+    align-items: center;
+    gap: 10px;
+}
+
+.suggested-list {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+}
+
+.suggested-item {
+    padding: 10px 15px;
+    background: rgba(255, 255, 255, 0.05);
+    border-radius: 8px;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    font-size: 0.95rem;
+}
+
+.suggested-item:hover {
+    background: rgba(0, 210, 255, 0.15);
+    transform: translateX(5px);
+}
+
+.keyword-tabs {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 10px;
+    padding: 15px;
+}
+
+.keyword-tab {
+    padding: 8px 15px;
+    background: rgba(58, 123, 213, 0.2);
+    border-radius: 20px;
+    font-size: 0.9rem;
+    cursor: pointer;
+    transition: all 0.2s ease;
+}
+
+.keyword-tab:hover {
+    background: rgba(58, 123, 213, 0.3);
+    transform: translateY(-2px);
+}
+
+.chat-input {
+    padding: 15px;
+    background: rgba(0, 0, 0, 0.2);
+    display: flex;
+    gap: 10px;
+}
+
+.chat-input textarea {
+    flex: 1;
+    padding: 12px 15px;
+    background: rgba(255, 255, 255, 0.05);
+    border: 1px solid rgba(255, 255, 255, 0.1);
+    border-radius: 25px;
+    width: 100%;
+    min-height: 48px !important;
+    height: 48px;
+    padding: 10px 10px !important;
+    resize: none;
+    font-size: 16px !important;
+    outline: none;
+    line-height: 24px;
+}
+
+.chat-input button {
+    width: 50px;
+    height: 50px;
+    border-radius: 50%;
+    background: linear-gradient(135deg, #00d2ff, #3a7bd5);
+    border: none;
+    color: white;
+    font-size: 1.2rem;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+/* 动画 */
+@keyframes fadeIn {
+    from {
+        opacity: 0;
+        transform: translateY(10px);
+    }
+
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+
+@media (max-width: 768px) {
+    .dashboard {
+        grid-template-columns: 1fr;
+    }
+
+    .full-width,
+    .half-width {
+        grid-column: span 1;
+    }
+
+    .header {
+        flex-direction: column;
+        gap: 20px;
+    }
+
+    .assistant-sidebar {
+        display: none;
+    }
+}
+</style>

+ 45 - 77
src/pages/Entry/index.vue

@@ -5,6 +5,7 @@ import { onMounted, ref } from "vue";
 import MapChart from "./components/MapChart/index.vue";
 import RealtimeData from "./components/RealtimeData/index.vue";
 import TimerCount from "./components/TimerCount/index.vue";
+import Right from "./components/homeRight/right.vue"
 
 // interface WeatherData {
 //   data: {
@@ -25,63 +26,27 @@ const pvUv = ref({
   uv: 12345,
 });
 
-const top10Questions = ref(
-  Array.from({ length: 10 }).map((_, index) => ({
-    keyword: `Q${index + 1}`,
-    count: 100 - 10 * index,
-  }))
-);
-
-
-
-const getWeather = async () => {
-  // const pList: Promise<MapPM25>[] = [];
-  // Object.entries(
-  //   cityCodeMap as {
-  //     [key: string]: { city_code: string; children?: { city_code: string }[] };
-  //   }
-  // ).forEach(([key, val]) => {
-  //   pList.push(
-  //     new Promise((resolve, reject) => {
-  //       $get(`/weather/${val.city_code || val.children?.[0].city_code}`)
-  //         .then((r: WeatherData) => {
-  //           // const forecast = r.data.forecast.slice(0, 1);
-  //           resolve({ code: key, value: r.data.pm25 });
-  //         })
-  //         .catch((e: Error) => reject(e));
-  //     })
-  //   );
-  // });
-  // const res = await Promise.all(pList);
-  mapData.value = [
-    { code: "110000", value: 200 },
-    { code: "120000", value: 210 },
-    { code: "140000", value: 220 },
-    { code: "150000", value: 230 },
-  ];
-  console.log("res :>> ", mapData.value);
-};
-
-onMounted(() => {
-  getWeather();
-});
+
+
+
 </script>
 
 <template>
   <section class="title">
-    <div class="title-content">应用概览</div>
-    <TimerCount />
+    <div class="title-content">
+      <h1>AI教室智慧控制中心</h1>
+    </div>
   </section>
-  <!-- <div>{{ tip }}</div> -->
-  <section class="content">
-    <section class="left">
+  <el-row :gutter="20" class="content">
+    <el-col :xl="12" :md="12" :sm="24" :xs="24">
       <RealtimeData :data="pvUv" />
-    </section>
-    <section class="middle">
+    </el-col>
+    <el-col :xl="12" :md="12" :sm="24" :xs="24">
       <MapChart :data="mapData" />
-    </section>
-    <section class="right">
-    </section>
+    </el-col>
+  </el-row>
+  <section class="right">
+    <Right />
   </section>
 </template>
 
@@ -89,34 +54,25 @@ onMounted(() => {
 @import "@assets/styles/common.less";
 
 .title {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  color: #222;
-  text-shadow: 0 0 5px #00ecff;
-  font-size: 66 * @px2vw;
+  padding: 20px 0;
+  margin-bottom: 30px;
 
-  &-content {
+  border-bottom: 2px solid rgba(255, 255, 255, 0.1);
+
+  .title-content {
     display: flex;
     align-items: center;
+    gap: 15px;
 
-    &::before,
-    &::after {
-      content: "";
-      width: 200 * @px2vw;
-      height: 3 * @px2vw;
-    }
-
-    &::before {
-      margin-right: 20 * @px2vw;
-      background-image: linear-gradient(to right, transparent, #156dae);
+    h1 {
+      color: transparent;
+      background: linear-gradient(to right, #00d2ff, #3a7bd5);
+      background-clip: text;
     }
+  }
 
-    &::after {
-      margin-left: 20 * @px2vw;
-      background-image: linear-gradient(to left, transparent, #156dae);
-    }
+  &-content {
+    display: flex;
   }
 }
 
@@ -124,21 +80,33 @@ onMounted(() => {
   flex: 1;
   display: flex;
   padding: 20 * @px2vw;
+  // max-width: 1400px;
+  margin-left: 0!important;
+  margin-right: 0!important;
+  max-width: calc(100vw - 400px);
 
-  .left,
-  .right {
+  .left {
     display: flex;
     flex-direction: column;
     flex-shrink: 0;
-    width: 600 * @px2vw;
   }
 
   .middle {
     flex: 1;
-    min-width: 600 * @px2vw;
-    min-height: 300 * @px2vw;
     padding-top: 20 * @px2vw;
     margin: 0 14 * @px2vw;
   }
+
+}
+
+.right {
+  position: fixed;
+  right: 0;
+  top: 0;
+  width: 400px;
+  background: rgba(20, 30, 48, 0.95);
+  border-left: 1px solid rgba(0, 210, 255, 0.2);
+  box-shadow: -5px 0 15px rgba(0, 0, 0, 0.3);
+  height: 100vh;
 }
 </style>

+ 53 - 0
src/service/socket/socket.ts

@@ -0,0 +1,53 @@
+import { onBeforeUnmount } from 'vue';
+
+function useMultipleWebSockets(urls: any) {
+  const sockets: { [key: string]: WebSocket } = {};
+  const status: { [key: string]: string } = {};
+  const sendData: { [key: string]: string } = {};
+
+  // 初始化所有连接
+  urls.forEach((url: string) => {
+    sockets[url] = new WebSocket(url);
+
+    sockets[url].onopen = () => {
+      console.log(`WebSocket connected to ${url}`);
+      status[url] = "connected";
+    };
+
+    sockets[url].onmessage = (event) => {
+      // console.log(`Message from ${url}:`, event.data);
+      sendData[url] = event.data;
+      // 处理不同来源的消息
+    };
+
+    sockets[url].onerror = (error) => {
+      console.error(`WebSocket error on ${url}:`, error);
+    };
+
+    sockets[url].onclose = () => {
+      console.log(`WebSocket disconnected from ${url}`);
+      status[url] = "disconnected";
+    };
+  });
+
+  // 组件卸载时关闭所有连接
+  onBeforeUnmount(() => {
+    Object.values(sockets).forEach(socket => {
+      if (socket.readyState === WebSocket.OPEN) {
+        socket.close();
+      }
+    });
+  });
+
+  // 发送消息到指定连接
+  const send = (url: any, message: any) => {
+    if (sockets[url] && sockets[url].readyState === WebSocket.OPEN) {
+      sockets[url].send(JSON.stringify(message));
+    }
+  };
+
+  return { sockets, send,sendData,status };
+}
+
+
+export default useMultipleWebSockets;

+ 1 - 2
src/style.css

@@ -9,6 +9,5 @@
   flex-direction: column;
   min-width: 100vw;
   min-height: 100vh;
-  background-color: #fff;
-  color: #00ECFF;
+  background: linear-gradient(135deg, #1a2a6c, #2c3e50);
 }

Some files were not shown because too many files changed in this diff