From 34900348ac27a7eab1a1499da57635fc0552af7f Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期三, 29 四月 2026 11:17:47 +0800
Subject: [PATCH] 进销存newpro 1.系统架构图前端初模型
---
src/views/systemArchitecture/index.vue | 436 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/router/index.js | 14 +
2 files changed, 450 insertions(+), 0 deletions(-)
diff --git a/src/router/index.js b/src/router/index.js
index 52c31d4..cc2b88c 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -47,6 +47,20 @@
component: () => import("@/views/register"),
hidden: true,
},
+ // 绯荤粺鏋舵瀯鍥�
+ {
+ path: "/system-architecture",
+ component: Layout,
+ redirect: "/system-architecture/index",
+ children: [
+ {
+ path: "index",
+ component: () => import("@/views/systemArchitecture/index.vue"),
+ name: "SystemArchitecture",
+ meta: { title: "绯荤粺鏋舵瀯鍥�", icon: "tree" },
+ },
+ ],
+ },
{
path: "/:pathMatch(.*)*",
component: () => import("@/views/error/404"),
diff --git a/src/views/systemArchitecture/index.vue b/src/views/systemArchitecture/index.vue
new file mode 100644
index 0000000..1eb37bb
--- /dev/null
+++ b/src/views/systemArchitecture/index.vue
@@ -0,0 +1,436 @@
+<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>
+ </div>
+ </section>
+ </section>
+ </div>
+</template>
+
+<script setup>
+import { nextTick, onBeforeUnmount, onMounted, onUpdated, ref } from 'vue'
+
+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 canvasRef = ref(null)
+const sourceNodeRef = ref(null)
+const targetNodeRef = ref(null)
+const linkPath = ref('')
+const svgSize = ref({ width: 0, height: 0 })
+let resizeObserver = null
+
+function setNodeRef(item) {
+ return (el) => {
+ if (item.linkSource) sourceNodeRef.value = el
+ if (item.linkTarget) targetNodeRef.value = el
+ }
+}
+
+function resetLine() {
+ linkPath.value = ''
+}
+
+function updateLinkLine() {
+ if (!canvasRef.value || !sourceNodeRef.value || !targetNodeRef.value || window.innerWidth <= 768) {
+ resetLine()
+ return
+ }
+
+ const canvasRect = canvasRef.value.getBoundingClientRect()
+ const sourceRect = sourceNodeRef.value.getBoundingClientRect()
+ const targetRect = targetNodeRef.value.getBoundingClientRect()
+
+ svgSize.value = {
+ width: Math.ceil(canvasRef.value.scrollWidth),
+ height: Math.ceil(canvasRef.value.scrollHeight)
+ }
+
+ 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)
+
+ linkPath.value = `M ${startX} ${startY} L ${startX} ${middleY} L ${endX} ${middleY} L ${endX} ${endY}`
+}
+
+function handleResize() {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(updateLinkLine)
+ })
+}
+
+onMounted(async () => {
+ await nextTick()
+ handleResize()
+ window.addEventListener('resize', handleResize)
+ if (window.ResizeObserver && canvasRef.value) {
+ resizeObserver = new ResizeObserver(handleResize)
+ resizeObserver.observe(canvasRef.value)
+ }
+})
+
+onUpdated(() => {
+ nextTick(handleResize)
+})
+
+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 {
+ width: 100%;
+ min-height: 52px;
+ flex-direction: row;
+ justify-content: flex-start;
+ gap: 10px;
+ }
+
+ .node h3 {
+ text-align: left;
+ }
+
+ .flow-arrow {
+ width: 1px;
+ height: 16px;
+ margin: 0 auto;
+ }
+
+ .flow-arrow::after {
+ top: auto;
+ bottom: -1px;
+ right: 50%;
+ transform: translateX(50%) rotate(135deg);
+ }
+}
+</style>
--
Gitblit v1.9.3