353 lines
9.8 KiB
Vue
353 lines
9.8 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="layered-architecture-demo">
|
|||
|
|
<div class="architecture-container">
|
|||
|
|
<!-- 客户端 -->
|
|||
|
|
<div class="client-layer">
|
|||
|
|
<div class="layer-box client">
|
|||
|
|
<div class="layer-icon">🌐</div>
|
|||
|
|
<div class="layer-title">客户端</div>
|
|||
|
|
<div class="layer-desc">Web / App / 小程序</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="arrow-down">⬇️ HTTP/HTTPS</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 后端分层 -->
|
|||
|
|
<div class="backend-layers">
|
|||
|
|
<!-- Controller 层 -->
|
|||
|
|
<div
|
|||
|
|
class="layer-box controller"
|
|||
|
|
:class="{ active: activeLayer === 'controller' }"
|
|||
|
|
@click="setActiveLayer('controller')"
|
|||
|
|
>
|
|||
|
|
<div class="layer-header">
|
|||
|
|
<span class="layer-icon">🎮</span>
|
|||
|
|
<span class="layer-name">Controller</span>
|
|||
|
|
<span class="layer-badge">入口</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="layer-content">
|
|||
|
|
<div class="duty">职责:接收请求、参数校验、调用 Service</div>
|
|||
|
|
<div class="tech">技术:Spring MVC / Gin / Echo</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="arrow-down">⬇️ 调用</div>
|
|||
|
|
|
|||
|
|
<!-- Service 层 -->
|
|||
|
|
<div
|
|||
|
|
class="layer-box service"
|
|||
|
|
:class="{ active: activeLayer === 'service' }"
|
|||
|
|
@click="setActiveLayer('service')"
|
|||
|
|
>
|
|||
|
|
<div class="layer-header">
|
|||
|
|
<span class="layer-icon">⚙️</span>
|
|||
|
|
<span class="layer-name">Service</span>
|
|||
|
|
<span class="layer-badge">业务核心</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="layer-content">
|
|||
|
|
<div class="duty">职责:业务逻辑编排、事务管理、跨模块协调</div>
|
|||
|
|
<div class="tech">技术:纯代码逻辑 / 无框架依赖</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="arrow-down">⬇️ 调用</div>
|
|||
|
|
|
|||
|
|
<!-- Repository 层 -->
|
|||
|
|
<div
|
|||
|
|
class="layer-box repository"
|
|||
|
|
:class="{ active: activeLayer === 'repository' }"
|
|||
|
|
@click="setActiveLayer('repository')"
|
|||
|
|
>
|
|||
|
|
<div class="layer-header">
|
|||
|
|
<span class="layer-icon">🗄️</span>
|
|||
|
|
<span class="layer-name">Repository</span>
|
|||
|
|
<span class="layer-badge">数据访问</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="layer-content">
|
|||
|
|
<div class="duty">职责:数据持久化、查询封装、ORM 映射</div>
|
|||
|
|
<div class="tech">技术:MyBatis / GORM / Hibernate</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="arrow-down">⬇️ SQL</div>
|
|||
|
|
|
|||
|
|
<!-- Domain 层 -->
|
|||
|
|
<div
|
|||
|
|
class="layer-box domain"
|
|||
|
|
:class="{ active: activeLayer === 'domain' }"
|
|||
|
|
@click="setActiveLayer('domain')"
|
|||
|
|
>
|
|||
|
|
<div class="layer-header">
|
|||
|
|
<span class="layer-icon">📦</span>
|
|||
|
|
<span class="layer-name">Domain / Model</span>
|
|||
|
|
<span class="layer-badge">领域模型</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="layer-content">
|
|||
|
|
<div class="duty">职责:实体定义、业务规则、值对象</div>
|
|||
|
|
<div class="tech">技术:POJO / Struct / Class</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="arrow-down">⬇️ 持久化</div>
|
|||
|
|
|
|||
|
|
<!-- 数据库 -->
|
|||
|
|
<div class="layer-box database">
|
|||
|
|
<div class="layer-icon">💾</div>
|
|||
|
|
<div class="layer-title">数据库</div>
|
|||
|
|
<div class="layer-desc">MySQL / PostgreSQL / MongoDB</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 右侧说明面板 -->
|
|||
|
|
<div class="info-panel" v-if="activeLayer">
|
|||
|
|
<h4>{{ layerInfo.title }}</h4>
|
|||
|
|
<p>{{ layerInfo.description }}</p>
|
|||
|
|
<div class="analogy">
|
|||
|
|
<strong>💡 类比:</strong>{{ layerInfo.analogy }}
|
|||
|
|
</div>
|
|||
|
|
<div class="common-mistakes">
|
|||
|
|
<strong>⚠️ 常见错误:</strong>
|
|||
|
|
<ul>
|
|||
|
|
<li v-for="mistake in layerInfo.mistakes" :key="mistake">{{ mistake }}</li>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 底部交互提示 -->
|
|||
|
|
<div class="interaction-hint">
|
|||
|
|
💡 点击各层查看详细说明 | 实际调用流向:从上到下,依赖从下到上
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, computed } from 'vue'
|
|||
|
|
|
|||
|
|
const activeLayer = ref('')
|
|||
|
|
|
|||
|
|
const setActiveLayer = (layer) => {
|
|||
|
|
activeLayer.value = activeLayer.value === layer ? '' : layer
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const layerInfo = computed(() => {
|
|||
|
|
const infoMap = {
|
|||
|
|
controller: {
|
|||
|
|
title: 'Controller 层 - 请求的"门童"',
|
|||
|
|
description: 'Controller 是系统的入口,负责接收 HTTP 请求、解析参数、进行基础校验,然后调用 Service 层处理业务。',
|
|||
|
|
analogy: '就像餐厅的门童,负责迎接客人(接收请求)、检查预约(参数校验)、引导入座(路由到对应服务),但不负责做菜。',
|
|||
|
|
mistakes: [
|
|||
|
|
'在 Controller 里写业务逻辑(应该放在 Service)',
|
|||
|
|
'直接操作数据库(应该调用 Repository)',
|
|||
|
|
'不做参数校验,导致脏数据流入系统'
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
service: {
|
|||
|
|
title: 'Service 层 - 业务逻辑的"厨师"',
|
|||
|
|
description: 'Service 是系统的核心,负责编排业务逻辑、管理事务、协调多个 Repository。这一层应该包含所有的业务规则和流程。',
|
|||
|
|
analogy: '就像餐厅的厨师,负责按照菜谱(业务规则)做菜,需要协调各种食材(数据),把控菜品质量(业务正确性)。',
|
|||
|
|
mistakes: [
|
|||
|
|
'Service 之间互相调用,形成循环依赖',
|
|||
|
|
'在 Service 里直接写 SQL(应该放在 Repository)',
|
|||
|
|
'一个 Service 方法过长,包含多个业务场景(应该拆分成多个方法)'
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
repository: {
|
|||
|
|
title: 'Repository 层 - 数据的"仓管"',
|
|||
|
|
description: 'Repository 负责与数据库交互,封装所有的 CRUD 操作。上层不需要关心具体的数据库类型和 SQL 语句。',
|
|||
|
|
analogy: '就像餐厅的仓管员,负责从仓库(数据库)取食材、存放剩余食材,厨师(Service)只需要告诉他要什么,不需要知道仓库在哪。',
|
|||
|
|
mistakes: [
|
|||
|
|
'在 Repository 里写业务逻辑(应该只负责数据访问)',
|
|||
|
|
'直接返回数据库实体给前端(应该转换为 DTO)',
|
|||
|
|
'一个 Repository 操作多个表(应该拆分到不同 Repository)'
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
domain: {
|
|||
|
|
title: 'Domain 层 - 业务概念的"蓝图"',
|
|||
|
|
description: 'Domain 定义了系统中的实体、值对象、业务规则。它是所有层的依赖基础,但不依赖任何其他层。',
|
|||
|
|
analogy: '就像餐厅的菜单和菜品标准,定义了什么是"宫保鸡丁"、用什么食材、什么口味。所有厨师都要按照这个标准来做。',
|
|||
|
|
mistakes: [
|
|||
|
|
'Domain 对象里包含持久化注解(应该保持纯净)',
|
|||
|
|
'在 Domain 里写业务逻辑(业务逻辑应该在 Service)',
|
|||
|
|
'Domain 对象之间循环依赖'
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return infoMap[activeLayer.value] || {}
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.layered-architecture-demo {
|
|||
|
|
padding: 20px;
|
|||
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
|
|||
|
|
border-radius: 12px;
|
|||
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.architecture-container {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 20px;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.client-layer {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.backend-layers {
|
|||
|
|
flex: 1;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.layer-box {
|
|||
|
|
padding: 16px;
|
|||
|
|
border-radius: 10px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
border: 2px solid transparent;
|
|||
|
|
background: white;
|
|||
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.layer-box:hover {
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.layer-box.active {
|
|||
|
|
border-color: #409eff;
|
|||
|
|
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 各层颜色主题 */
|
|||
|
|
.controller { border-left: 4px solid #67c23a; }
|
|||
|
|
.service { border-left: 4px solid #e6a23c; }
|
|||
|
|
.repository { border-left: 4px solid #409eff; }
|
|||
|
|
.domain { border-left: 4px solid #909399; }
|
|||
|
|
.client { border-left: 4px solid #f56c6c; }
|
|||
|
|
.database { border-left: 4px solid #8e44ad; }
|
|||
|
|
|
|||
|
|
.layer-header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 10px;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.layer-icon {
|
|||
|
|
font-size: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.layer-name {
|
|||
|
|
font-weight: 600;
|
|||
|
|
font-size: 15px;
|
|||
|
|
color: #303133;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.layer-badge {
|
|||
|
|
padding: 2px 8px;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
font-size: 11px;
|
|||
|
|
background: #f0f2f5;
|
|||
|
|
color: #606266;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.layer-content {
|
|||
|
|
padding-left: 30px;
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: #606266;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.duty, .tech {
|
|||
|
|
margin: 4px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.arrow-down {
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 6px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #909399;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-panel {
|
|||
|
|
width: 320px;
|
|||
|
|
padding: 20px;
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 10px;
|
|||
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|||
|
|
position: sticky;
|
|||
|
|
top: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-panel h4 {
|
|||
|
|
margin: 0 0 12px 0;
|
|||
|
|
color: #303133;
|
|||
|
|
font-size: 16px;
|
|||
|
|
padding-bottom: 10px;
|
|||
|
|
border-bottom: 2px solid #409eff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-panel p {
|
|||
|
|
margin: 0 0 16px 0;
|
|||
|
|
color: #606266;
|
|||
|
|
font-size: 13px;
|
|||
|
|
line-height: 1.7;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.analogy, .common-mistakes {
|
|||
|
|
margin: 12px 0;
|
|||
|
|
padding: 12px;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.analogy {
|
|||
|
|
background: #f0f9ff;
|
|||
|
|
border-left: 3px solid #409eff;
|
|||
|
|
color: #1d4ed8;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.common-mistakes {
|
|||
|
|
background: #fff2f0;
|
|||
|
|
border-left: 3px solid #ff4d4f;
|
|||
|
|
color: #cf1322;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.common-mistakes ul {
|
|||
|
|
margin: 6px 0 0 0;
|
|||
|
|
padding-left: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.common-mistakes li {
|
|||
|
|
margin: 4px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.interaction-hint {
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 16px;
|
|||
|
|
margin-top: 16px;
|
|||
|
|
background: #f6ffed;
|
|||
|
|
border: 1px solid #b7eb8f;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
color: #389e0d;
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 1024px) {
|
|||
|
|
.architecture-container {
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-panel {
|
|||
|
|
width: 100%;
|
|||
|
|
position: static;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|