Files
test-repo/docs/.vitepress/theme/components/appendix/backend-layered-architecture/LayeredArchitectureDemo.vue
T

399 lines
10 KiB
Vue
Raw Normal View History

<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
v-if="activeLayer"
class="info-panel"
>
<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: 6px;
color: #389e0d;
font-size: 13px;
}
@media (max-width: 1024px) {
.architecture-container {
flex-direction: column;
}
.info-panel {
width: 100%;
position: static;
}
}
</style>