From d3d2f9861c3d4bccfb9594b7efd76e62d3b44b65 Mon Sep 17 00:00:00 2001
From: yuan <123@>
Date: 星期三, 03 六月 2026 16:41:31 +0800
Subject: [PATCH] feat: 添加合格率计算及显示功能

---
 src/views/systemArchitecture/index.vue |  784 +++++++++++++++++++++++++++----------------------------
 1 files changed, 379 insertions(+), 405 deletions(-)

diff --git a/src/views/systemArchitecture/index.vue b/src/views/systemArchitecture/index.vue
index 1eb37bb..6ed31f6 100644
--- a/src/views/systemArchitecture/index.vue
+++ b/src/views/systemArchitecture/index.vue
@@ -1,436 +1,410 @@
 <template>
-  <div class="architecture-page">
-    <section ref="canvasRef" class="canvas">
-      <svg
-        v-if="linkPath"
-        class="link-overlay"
-        :width="svgSize.width"
-        :height="svgSize.height"
-        aria-hidden="true"
-      >
-        <defs>
-          <marker
-            id="erp-link-arrow"
-            markerWidth="10"
-            markerHeight="10"
-            refX="7"
-            refY="3.5"
-            orient="auto"
-            markerUnits="strokeWidth"
-          >
-            <path d="M0,0 L0,7 L7,3.5 z" fill="#94a3b8" />
-          </marker>
-        </defs>
-        <path
-          :d="linkPath"
-          class="link-overlay__path"
-          marker-end="url(#erp-link-arrow)"
-        />
-      </svg>
-
-      <section
-        v-for="row in architectureRows"
-        :key="row.key"
-        class="lane"
-      >
-        <aside class="lane__aside">
-          <svg-icon :icon-class="row.icon" class="lane__icon" />
-          <h2>{{ row.label }}</h2>
-          <span class="lane__arrow"></span>
-        </aside>
-
-        <div class="lane__flow">
-          <div
-            v-for="(item, index) in row.items"
-            :key="item.name"
-            class="node-wrap"
-          >
-            <article
-              class="node"
-              :class="{ 'node--accent': item.accent }"
-              :ref="setNodeRef(item)"
-            >
-              <span class="node__mark">
-                <span class="node__cap"></span>
-                <span class="node__base"></span>
-                <svg-icon :icon-class="item.icon" class="node__icon" />
-              </span>
-              <h3>{{ item.name }}</h3>
-            </article>
-            <span v-if="index < row.items.length - 1" class="flow-arrow"></span>
+  <div class="scale-container">
+    <div class="scroll-content"
+         :style="{ 
+           height: `${designHeight * scaleRatio}px`,
+           width: `${designWidth * scaleRatio}px` 
+         }">
+      <div class="architecture-scaling-container"
+           :style="{ transform: `scale(${scaleRatio})` }">
+        <div class="architecture-container">
+          <div class="header">
+            <span class="title-bar"></span>
+            <h1 class="title-text">绯荤粺鏋舵瀯鍥�</h1>
+          </div>
+          <div class="image-wrapper">
+            <div class="hotspot-container">
+              <img :src="architectureImg"
+                   alt="绯荤粺鏋舵瀯鍥�"
+                   class="architecture-img" />
+              <!-- 鐑偣鍖哄煙 -->
+              <div v-for="spot in hotspots"
+                   :key="spot.id"
+                   class="hotspot"
+                   :style="{ top: spot.top, left: spot.left, width: spot.width, height: spot.height }"
+                   @click="handleSpotClick(spot)"
+                   :title="'璺宠浆鑷�: ' + spot.name">
+              </div>
+            </div>
           </div>
         </div>
-      </section>
-    </section>
+      </div>
+    </div>
   </div>
 </template>
 
 <script setup>
-import { nextTick, onBeforeUnmount, onMounted, onUpdated, ref } from 'vue'
+  import { ref, onMounted, onBeforeUnmount, nextTick } from "vue";
+  import { useRouter } from "vue-router";
+  import architectureImg from "@/assets/images/xitongjiagou.svg";
 
-const architectureRows = [
-  {
-    key: 'basic',
-    label: '鍩虹閰嶇疆',
-    icon: 'system',
-    items: [
-      { name: '瑙掕壊鐢ㄦ埛绠$悊', icon: 'user' },
-      { name: '浜у搧缁存姢', icon: 'monitor' },
-      { name: '瀹℃壒绠$悊', icon: 'tree', accent: true }
-    ]
-  },
-  {
-    key: 'sale',
-    label: '閿�鍞�',
-    icon: 'chart',
-    items: [
-      { name: '瀹㈡埛妗f', icon: 'peoples' },
-      { name: '閿�鍞姤浠�', icon: 'form' },
-      { name: '閿�鍞彴璐�', icon: 'table' },
-      { name: '鍙戣揣鍙拌处', icon: 'clipboard', accent: true, linkSource: true },
-      { name: '閿�鍞��璐�', icon: 'nested' },
-      { name: '瀹㈡埛寰�鏉�', icon: 'money' },
-      { name: '鎸囨爣缁熻', icon: 'chart' }
-    ]
-  },
-  {
-    key: 'purchase',
-    label: '閲囪喘',
-    icon: 'shopping',
-    items: [
-      { name: '渚涘簲鍟嗘。妗�', icon: 'people' },
-      { name: '閲囪喘鍙拌处', icon: 'table', accent: true },
-      { name: '閲囪喘閫�璐�', icon: 'nested' },
-      { name: '渚涘簲鍟嗗線鏉�', icon: 'money' },
-      { name: '閲囪喘鎶ヨ〃', icon: 'chart' }
-    ]
-  },
-  {
-    key: 'produce',
-    label: '鐢熶骇',
-    icon: 'build',
-    items: [
-      { name: '宸ュ簭', icon: 'tree' },
-      { name: 'BOM', icon: 'list' },
-      { name: '宸ヨ壓璺嚎', icon: 'guide' },
-      { name: '鐢熶骇璁㈠崟', icon: 'form' },
-      { name: '鐢熶骇鎺掍骇', icon: 'date' },
-      { name: '鐢熶骇鎶ュ伐', icon: 'edit' },
-      { name: '鎶ュ伐鍙拌处', icon: 'clipboard' },
-      { name: '鐢熶骇鏍哥畻', icon: 'money', accent: true }
-    ]
-  },
-  {
-    key: 'store',
-    label: '浠撳偍鐗╂祦',
-    icon: 'redis',
-    items: [
-      { name: '鍏ュ簱绠$悊', icon: 'download', accent: true },
-      { name: '鍑哄簱绠$悊', icon: 'upload', accent: true, linkTarget: true },
-      { name: '搴撳瓨绠$悊', icon: 'redis-list' }
-    ]
-  }
-]
+  const router = useRouter();
 
-const canvasRef = ref(null)
-const sourceNodeRef = ref(null)
-const targetNodeRef = ref(null)
-const linkPath = ref('')
-const svgSize = ref({ width: 0, height: 0 })
-let resizeObserver = null
+  // 鐑偣鍖哄煙瀹氫箟
+  const hotspots = ref([
+    {
+      id: 1,
+      name: "瑙掕壊绠$悊",
+      top: "3%",
+      left: "19%",
+      width: "9%",
+      height: "11%",
+      path: "/system/role",
+    },
+    {
+      id: 2,
+      name: "鑱屼笟绠$悊",
+      top: "1.6%",
+      left: "36.5%",
+      width: "9%",
+      height: "11%",
+      path: "/system/user",
+    },
+    {
+      id: 3,
+      name: "宀椾綅绠$悊",
+      top: "1.6%",
+      left: "56%",
+      width: "9%",
+      height: "11%",
+      path: "/system/post",
+    },
+    {
+      id: 4,
+      name: "閮ㄩ棬绠$悊",
+      top: "3%",
+      left: "73%",
+      width: "9%",
+      height: "11%",
+      path: "/system/dept",
+    },
+    {
+      id: 5,
+      name: "瀹㈡埛妗f",
+      top: "42%",
+      left: "17%",
+      width: "9%",
+      height: "3%",
+      path: "/salesManagement/customerFileOpenSea",
+    },
+    {
+      id: 6,
+      name: "閿�鍞姤浠�",
+      top: "46%",
+      left: "17%",
+      width: "9%",
+      height: "3%",
+      path: "/salesManagement/salesQuotation",
+    },
+    {
+      id: 7,
+      name: "閿�鍞彴璐�",
+      top: "51%",
+      left: "17%",
+      width: "9%",
+      height: "3%",
+      path: "/salesManagement/salesLedger",
+    },
+    {
+      id: 8,
+      name: "鍙戣揣鍙拌处",
+      top: "55%",
+      left: "17%",
+      width: "9%",
+      height: "3%",
+      path: "/salesManagement/deliveryLedger",
+    },
+    {
+      id: 9,
+      name: "閿�鍞��璐�",
+      top: "59%",
+      left: "17%",
+      width: "9%",
+      height: "3%",
+      path: "/salesManagement/returnOrder",
+    },
+    {
+      id: 10,
+      name: "渚涘簲鍟嗘。妗�",
+      top: "42%",
+      left: "75%",
+      width: "9%",
+      height: "3%",
+      path: "/salesManagement/customerFileOpenSea",
+    },
+    {
+      id: 11,
+      name: "閲囪喘鍙拌处",
+      top: "47%",
+      left: "75%",
+      width: "9%",
+      height: "3%",
+      path: "/procurementManagement/procurementLedger",
+    },
+    {
+      id: 12,
+      name: "閲囪喘閫�璐�",
+      top: "51%",
+      left: "75%",
+      width: "9%",
+      height: "3%",
+      path: "/procurementManagement/purchaseReturnOrder",
+    },
+    {
+      id: 13,
+      name: "渚涘簲鍟嗗線鏉�",
+      top: "56%",
+      left: "75%",
+      width: "9%",
+      height: "3%",
+      path: "/procurementManagement/paymentLedger",
+    },
+    {
+      id: 14,
+      name: "閲囪喘鎶ヨ〃",
+      top: "60%",
+      left: "75%",
+      width: "9%",
+      height: "3%",
+      path: "/procurementManagement/procurementReport",
+    },
+    {
+      id: 15,
+      name: "鐢熶骇鎺掍骇",
+      top: "77%",
+      left: "19%",
+      width: "15%",
+      height: "3%",
+      path: "/productionManagement/productionManagement/workOrderEdit/index",
+    },
+    {
+      id: 16,
+      name: "鐢熶骇璁㈠崟",
+      top: "76%",
+      left: "43%",
+      width: "15%",
+      height: "3%",
+      path: "/productionManagement/productionOrder",
+    },
+    {
+      id: 17,
+      name: "bom",
+      top: "77%",
+      left: "70%",
+      width: "15%",
+      height: "3%",
+      path: "/processDesign/productionManagement/productStructure/index",
+    },
+    {
+      id: 18,
+      name: "鐢熶骇璁㈠崟",
+      top: "82%",
+      left: "32%",
+      width: "15%",
+      height: "3%",
+      path: "/productionManagement/productionOrder",
+    },
+    {
+      id: 19,
+      name: "宸ヨ壓璺嚎",
+      top: "82%",
+      left: "57%",
+      width: "15%",
+      height: "3%",
+      path: "/processDesign/processRoute",
+    },
+    {
+      id: 20,
+      name: "宸ュ簭",
+      top: "87%",
+      left: "17%",
+      width: "9%",
+      height: "4%",
+      path: "/processDesign/productionManagement/productionProcess/index",
+    },
+    {
+      id: 21,
+      name: "鐢熶骇鎶ュ伐",
+      top: "89%",
+      left: "32%",
+      width: "17%",
+      height: "4%",
+      path: "/productionManagement/workOrderManagement",
+    },
+    {
+      id: 22,
+      name: "鐢熶骇鏍哥畻",
+      top: "89%",
+      left: "56%",
+      width: "19%",
+      height: "4%",
+      path: "/productionManagement/productionCosting",
+    },
+  ]);
 
-function setNodeRef(item) {
-  return (el) => {
-    if (item.linkSource) sourceNodeRef.value = el
-    if (item.linkTarget) targetNodeRef.value = el
-  }
-}
+  const handleSpotClick = spot => {
+    if (spot.path) {
+      router.push(spot.path);
+    }
+  };
 
-function resetLine() {
-  linkPath.value = ''
-}
+  // 缂╂斁姣斾緥
+  const scaleRatio = ref(1);
+  // 璁捐灏哄锛堝熀鍑嗗昂瀵革級
+  const designWidth = 1920;
+  const designHeight = 980;
 
-function updateLinkLine() {
-  if (!canvasRef.value || !sourceNodeRef.value || !targetNodeRef.value || window.innerWidth <= 768) {
-    resetLine()
-    return
-  }
+  // 璁$畻缂╂斁姣斾緥
+  const calculateScale = () => {
+    const container = document.querySelector(".scale-container");
+    if (!container) return;
 
-  const canvasRect = canvasRef.value.getBoundingClientRect()
-  const sourceRect = sourceNodeRef.value.getBoundingClientRect()
-  const targetRect = targetNodeRef.value.getBoundingClientRect()
+    // 鑾峰彇瀹瑰櫒鐨勫疄闄呭昂瀵�
+    const containerWidth = container.clientWidth;
+    const containerHeight = container.clientHeight;
 
-  svgSize.value = {
-    width: Math.ceil(canvasRef.value.scrollWidth),
-    height: Math.ceil(canvasRef.value.scrollHeight)
-  }
+    if (containerWidth === 0 || containerHeight === 0) {
+      // 濡傛灉杩樻病娓叉煋鍑烘潵锛屽欢杩熶竴涓嬪啀璁$畻
+      setTimeout(calculateScale, 100);
+      return;
+    }
 
-  const startX = sourceRect.left + sourceRect.width / 2 - canvasRect.left
-  const startY = sourceRect.bottom - canvasRect.top + 2
-  const endX = targetRect.left + targetRect.width / 2 - canvasRect.left
-  const endY = targetRect.top - canvasRect.top - 6
-  const middleY = startY + Math.max(24, (endY - startY) / 2)
+    // 璁$畻缂╂斁姣斾緥锛屽彇瀹介珮姣斾緥涓殑杈冨ぇ鍊硷紝纭繚濮嬬粓鎾戞弧鏁翠釜瀹瑰櫒
+    const scaleX = containerWidth / designWidth;
+    const scaleY = containerHeight / designHeight;
+    scaleRatio.value = Math.max(scaleX, scaleY);
+  };
 
-  linkPath.value = `M ${startX} ${startY} L ${startX} ${middleY} L ${endX} ${middleY} L ${endX} ${endY}`
-}
+  // 绐楀彛澶у皬鍙樺寲澶勭悊
+  const handleResize = () => {
+    calculateScale();
+  };
 
-function handleResize() {
-  requestAnimationFrame(() => {
-    requestAnimationFrame(updateLinkLine)
-  })
-}
+  // 鐢熷懡鍛ㄦ湡
+  let resizeObserver = null;
+  onMounted(() => {
+    // 璁$畻鍒濆缂╂斁姣斾緥
+    nextTick(() => {
+      calculateScale();
+    });
 
-onMounted(async () => {
-  await nextTick()
-  handleResize()
-  window.addEventListener('resize', handleResize)
-  if (window.ResizeObserver && canvasRef.value) {
-    resizeObserver = new ResizeObserver(handleResize)
-    resizeObserver.observe(canvasRef.value)
-  }
-})
+    // 鐩戝惉绐楀彛澶у皬鍙樺寲
+    window.addEventListener("resize", handleResize);
 
-onUpdated(() => {
-  nextTick(handleResize)
-})
+    // 浣跨敤 ResizeObserver 鐩戝惉瀹瑰櫒灏哄鍙樺寲锛堣В鍐充晶杈规爮鍒囨崲闂锛�
+    const container = document.querySelector(".scale-container");
+    if (container && window.ResizeObserver) {
+      resizeObserver = new ResizeObserver(() => {
+        calculateScale();
+      });
+      resizeObserver.observe(container);
+    }
+  });
 
-onBeforeUnmount(() => {
-  window.removeEventListener('resize', handleResize)
-  if (resizeObserver) resizeObserver.disconnect()
-})
+  onBeforeUnmount(() => {
+    window.removeEventListener("resize", handleResize);
+    if (resizeObserver) {
+      resizeObserver.disconnect();
+    }
+  });
 </script>
 
-<style scoped>
-.architecture-page {
-  min-height: calc(100vh - 84px);
-  padding: 18px;
-  background: #f6f7f9;
-}
-
-.canvas {
-  position: relative;
-  display: grid;
-  gap: 10px;
-}
-
-.lane {
-  display: grid;
-  grid-template-columns: 94px 1fr;
-  gap: 10px;
-  align-items: stretch;
-}
-
-.lane__aside {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  gap: 8px;
-  padding: 8px 6px;
-}
-
-.lane__icon {
-  font-size: 18px;
-  color: #2563eb;
-  flex-shrink: 0;
-}
-
-.lane__aside h2 {
-  margin: 0;
-  width: 34px;
-  font-size: 12px;
-  line-height: 1.15;
-  font-weight: 600;
-  color: #1f2937;
-  text-align: center;
-}
-
-.lane__arrow {
-  position: relative;
-  width: 16px;
-  height: 1px;
-  background: #cbd5e1;
-  flex-shrink: 0;
-}
-
-.lane__arrow::after {
-  content: '';
-  position: absolute;
-  top: 50%;
-  right: -1px;
-  width: 6px;
-  height: 6px;
-  border-top: 1px solid #cbd5e1;
-  border-right: 1px solid #cbd5e1;
-  transform: translateY(-50%) rotate(45deg);
-}
-
-.lane__flow {
-  position: relative;
-  z-index: 2;
-  display: flex;
-  flex-wrap: wrap;
-  align-items: center;
-  gap: 8px;
-  padding: 12px 14px;
-  background: #f1f3f5;
-}
-
-.node-wrap {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  position: relative;
-}
-
-.node {
-  width: 100px;
-  min-height: 72px;
-  padding: 4px 2px;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  gap: 6px;
-}
-
-.node__mark {
-  position: relative;
-  width: 34px;
-  height: 28px;
-  display: flex;
-  align-items: flex-start;
-  justify-content: center;
-}
-
-.node__cap {
-  position: absolute;
-  top: 0;
-  width: 18px;
-  height: 14px;
-  border-radius: 4px 4px 3px 3px;
-  background: linear-gradient(180deg, #60a5fa, #2563eb);
-  box-shadow: 0 3px 6px rgba(37, 99, 235, 0.16);
-}
-
-.node__base {
-  position: absolute;
-  bottom: 0;
-  width: 30px;
-  height: 12px;
-  border-radius: 4px;
-  background: linear-gradient(180deg, #ffffff, #e8edf3);
-  border: 1px solid #d7dee7;
-}
-
-.node__icon {
-  position: relative;
-  z-index: 1;
-  margin-top: 2px;
-  font-size: 15px;
-  color: #ffffff;
-}
-
-.node--accent .node__cap {
-  background: linear-gradient(180deg, #2dd4bf, #0f766e);
-  box-shadow: 0 3px 6px rgba(15, 118, 110, 0.16);
-}
-
-.node h3 {
-  margin: 0;
-  font-size: 12px;
-  line-height: 1.3;
-  font-weight: 600;
-  color: #111827;
-  text-align: center;
-}
-
-.flow-arrow {
-  width: 18px;
-  height: 1px;
-  background: #b6c1ce;
-  position: relative;
-  flex-shrink: 0;
-}
-
-.flow-arrow::after {
-  content: '';
-  position: absolute;
-  top: 50%;
-  right: -1px;
-  width: 6px;
-  height: 6px;
-  border-top: 1px solid #94a3b8;
-  border-right: 1px solid #94a3b8;
-  transform: translateY(-50%) rotate(45deg);
-}
-
-.link-overlay {
-  position: absolute;
-  inset: 0;
-  z-index: 20;
-  pointer-events: none;
-  overflow: visible;
-}
-
-.link-overlay__path {
-  fill: none;
-  stroke: #94a3b8;
-  stroke-width: 1;
-  stroke-dasharray: 4 4;
-  stroke-linecap: round;
-  stroke-linejoin: round;
-}
-
-@media (max-width: 1180px) {
-  .lane {
-    grid-template-columns: 1fr;
-    gap: 6px;
-  }
-
-  .lane__aside {
-    justify-content: flex-start;
-  }
-}
-
-@media (max-width: 768px) {
-  .architecture-page {
-    padding: 12px;
-  }
-
-  .lane__flow {
-    display: grid;
-    gap: 8px;
-    padding: 12px;
-  }
-
-  .node-wrap {
-    display: grid;
-    gap: 8px;
-  }
-
-  .node {
+<style scoped lang="scss">
+  .scale-container {
+    position: relative;
     width: 100%;
-    min-height: 52px;
-    flex-direction: row;
-    justify-content: flex-start;
-    gap: 10px;
+    height: calc(100vh - 84px);
+    background-color: #f5f7fa;
+    overflow: auto;
+    display: flex;
+    justify-content: center;
+    align-items: flex-start;
   }
 
-  .node h3 {
-    text-align: left;
+  .scroll-content {
+    position: relative;
+    flex-shrink: 0;
   }
 
-  .flow-arrow {
-    width: 1px;
-    height: 16px;
-    margin: 0 auto;
+  .architecture-scaling-container {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 1920px;
+    height: 980px;
+    transform-origin: left top;
+    background-color: #f5f7fa;
+    transition: transform 0.3s;
   }
 
-  .flow-arrow::after {
-    top: auto;
-    bottom: -1px;
-    right: 50%;
-    transform: translateX(50%) rotate(135deg);
+  .architecture-container {
+    padding: 20px;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+
+    .header {
+      display: flex;
+      align-items: center;
+      margin-bottom: 20px;
+      padding: 10px 20px;
+      background: #fff;
+      border-radius: 8px;
+      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+
+      .title-bar {
+        width: 4px;
+        height: 20px;
+        background-color: #409eff;
+        margin-right: 12px;
+        border-radius: 2px;
+      }
+
+      .title-text {
+        margin: 0;
+        font-size: 20px;
+        font-weight: 600;
+        color: #303133;
+      }
+    }
+
+    .image-wrapper {
+      flex: 1;
+      background: #fff;
+      border-radius: 8px;
+      padding: 20px 40px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      overflow: hidden;
+      box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+
+      .hotspot-container {
+        position: relative;
+        display: inline-block;
+        max-width: 100%;
+        max-height: 100%;
+
+        .architecture-img {
+          display: block;
+          max-width: 100%;
+          max-height: 100%;
+          object-fit: contain;
+        }
+
+        .hotspot {
+          position: absolute;
+          cursor: pointer;
+          background: rgba(64, 158, 255, 0);
+          transition: background 0.3s;
+          border-radius: 4px;
+
+          // &:hover {
+          //   background: rgba(64, 158, 255, 0.15);
+          //   box-shadow: 0 0 10px rgba(64, 158, 255, 0.3);
+          // }
+        }
+      }
+    }
   }
-}
 </style>

--
Gitblit v1.9.3