2026-02-06 03:34:50 +08:00
|
|
|
|
<template>
|
2026-03-01 12:28:47 +08:00
|
|
|
|
<div class="layered-arch-demo">
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<div class="title">后端四层架构总览</div>
|
|
|
|
|
|
<div class="subtitle">点击各层查看详细说明</div>
|
|
|
|
|
|
</div>
|
2026-02-06 03:34:50 +08:00
|
|
|
|
|
2026-03-01 12:28:47 +08:00
|
|
|
|
<div class="main">
|
|
|
|
|
|
<div class="layers">
|
|
|
|
|
|
<div class="client-box">客户端 (Web / App)</div>
|
|
|
|
|
|
<div class="arrow">↓ HTTP</div>
|
2026-02-06 03:34:50 +08:00
|
|
|
|
|
|
|
|
|
|
<div
|
2026-03-01 12:28:47 +08:00
|
|
|
|
v-for="layer in layers"
|
|
|
|
|
|
:key="layer.id"
|
|
|
|
|
|
:class="['layer-box', layer.id, { active: active === layer.id }]"
|
|
|
|
|
|
@click="active = active === layer.id ? '' : layer.id"
|
2026-02-06 03:34:50 +08:00
|
|
|
|
>
|
|
|
|
|
|
<div class="layer-header">
|
2026-03-01 12:28:47 +08:00
|
|
|
|
<span class="layer-name">{{ layer.name }}</span>
|
|
|
|
|
|
<span class="layer-badge">{{ layer.badge }}</span>
|
2026-02-06 03:34:50 +08:00
|
|
|
|
</div>
|
2026-03-01 12:28:47 +08:00
|
|
|
|
<div class="layer-duty">{{ layer.duty }}</div>
|
2026-02-06 03:34:50 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-01 12:28:47 +08:00
|
|
|
|
<div class="arrow">↓ SQL</div>
|
|
|
|
|
|
<div class="client-box db">数据库 (MySQL / PostgreSQL)</div>
|
2026-02-06 03:34:50 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-01 12:28:47 +08:00
|
|
|
|
<div v-if="active" class="info-panel">
|
|
|
|
|
|
<div class="info-title">{{ activeInfo.title }}</div>
|
|
|
|
|
|
<p>{{ activeInfo.desc }}</p>
|
|
|
|
|
|
<div class="info-analogy">{{ activeInfo.analogy }}</div>
|
|
|
|
|
|
<div class="info-mistakes">
|
|
|
|
|
|
<strong>常见错误:</strong>
|
2026-02-06 03:34:50 +08:00
|
|
|
|
<ul>
|
2026-03-01 12:28:47 +08:00
|
|
|
|
<li v-for="m in activeInfo.mistakes" :key="m">{{ m }}</li>
|
2026-02-06 03:34:50 +08:00
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, computed } from 'vue'
|
|
|
|
|
|
|
2026-03-01 12:28:47 +08:00
|
|
|
|
const active = ref('')
|
|
|
|
|
|
|
|
|
|
|
|
const layers = [
|
|
|
|
|
|
{ id: 'controller', name: 'Controller', badge: '入口', duty: '接收请求、参数校验、调用 Service' },
|
|
|
|
|
|
{ id: 'service', name: 'Service', badge: '业务核心', duty: '业务逻辑编排、事务管理、跨模块协调' },
|
|
|
|
|
|
{ id: 'repository', name: 'Repository', badge: '数据访问', duty: '数据持久化、查询封装、ORM 映射' },
|
|
|
|
|
|
{ id: 'domain', name: 'Domain', badge: '领域模型', duty: '实体定义、业务规则、值对象' }
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const infoMap = {
|
|
|
|
|
|
controller: {
|
|
|
|
|
|
title: 'Controller 层 — 请求的"门童"',
|
|
|
|
|
|
desc: '负责接收 HTTP 请求、解析参数、进行基础校验,然后调用 Service 层处理业务。',
|
|
|
|
|
|
analogy: '就像餐厅的门童,负责迎接客人、检查预约、引导入座,但不负责做菜。',
|
|
|
|
|
|
mistakes: ['在 Controller 里写业务逻辑', '直接操作数据库', '不做参数校验']
|
|
|
|
|
|
},
|
|
|
|
|
|
service: {
|
|
|
|
|
|
title: 'Service 层 — 业务逻辑的"厨师"',
|
|
|
|
|
|
desc: '编排业务逻辑、管理事务、协调多个 Repository。包含所有的业务规则和流程。',
|
|
|
|
|
|
analogy: '就像餐厅的厨师,按照菜谱做菜,协调各种食材,把控菜品质量。',
|
|
|
|
|
|
mistakes: ['Service 之间循环依赖', '直接写 SQL', '单个方法过长包含多个业务场景']
|
|
|
|
|
|
},
|
|
|
|
|
|
repository: {
|
|
|
|
|
|
title: 'Repository 层 — 数据的"仓管"',
|
|
|
|
|
|
desc: '封装所有数据访问逻辑,上层不需要关心具体的数据库类型和 SQL 语句。',
|
|
|
|
|
|
analogy: '就像仓管员,负责从仓库取食材、存放剩余食材,厨师只需说要什么。',
|
|
|
|
|
|
mistakes: ['在 Repository 里写业务逻辑', '直接返回实体给前端', '一个 Repository 操作多个表']
|
|
|
|
|
|
},
|
|
|
|
|
|
domain: {
|
|
|
|
|
|
title: 'Domain 层 — 业务概念的"蓝图"',
|
|
|
|
|
|
desc: '定义实体、值对象、业务规则。是所有层的依赖基础,但不依赖任何其他层。',
|
|
|
|
|
|
analogy: '就像菜单和菜品标准,定义了什么是"宫保鸡丁"、用什么食材、什么口味。',
|
|
|
|
|
|
mistakes: ['Domain 包含持久化注解', '在 Domain 里写数据库操作', 'Domain 对象之间循环依赖']
|
|
|
|
|
|
}
|
2026-02-06 03:34:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-01 12:28:47 +08:00
|
|
|
|
const activeInfo = computed(() => infoMap[active.value] || {})
|
2026-02-06 03:34:50 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-03-01 12:28:47 +08:00
|
|
|
|
.layered-arch-demo {
|
2026-02-06 03:34:50 +08:00
|
|
|
|
padding: 20px;
|
2026-03-01 12:28:47 +08:00
|
|
|
|
background: var(--vp-c-bg-soft);
|
2026-02-06 03:34:50 +08:00
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
}
|
2026-03-01 12:28:47 +08:00
|
|
|
|
.header { text-align: center; margin-bottom: 20px; }
|
|
|
|
|
|
.title { font-size: 16px; font-weight: 600; color: var(--vp-c-text-1); }
|
|
|
|
|
|
.subtitle { font-size: 13px; color: var(--vp-c-text-3); margin-top: 4px; }
|
2026-02-06 03:34:50 +08:00
|
|
|
|
|
2026-03-01 12:28:47 +08:00
|
|
|
|
.main { display: flex; gap: 20px; align-items: flex-start; }
|
|
|
|
|
|
.layers { flex: 1; display: flex; flex-direction: column; gap: 6px; }
|
2026-02-06 03:34:50 +08:00
|
|
|
|
|
2026-03-01 12:28:47 +08:00
|
|
|
|
.client-box {
|
|
|
|
|
|
padding: 12px; text-align: center; border-radius: 8px;
|
|
|
|
|
|
background: var(--vp-c-bg); color: var(--vp-c-text-2);
|
|
|
|
|
|
font-size: 13px; border: 1px solid var(--vp-c-divider);
|
2026-02-06 03:34:50 +08:00
|
|
|
|
}
|
2026-03-01 12:28:47 +08:00
|
|
|
|
.client-box.db { border-left: 3px solid #8b5cf6; }
|
|
|
|
|
|
.arrow { text-align: center; color: var(--vp-c-text-3); font-size: 12px; padding: 2px; }
|
2026-02-06 03:34:50 +08:00
|
|
|
|
|
|
|
|
|
|
.layer-box {
|
2026-03-01 12:28:47 +08:00
|
|
|
|
padding: 14px; border-radius: 8px; cursor: pointer;
|
|
|
|
|
|
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
border-left: 3px solid var(--vp-c-divider);
|
|
|
|
|
|
transition: all .2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.layer-box:hover { box-shadow: 0 2px 8px rgba(0,0,0,.06); }
|
|
|
|
|
|
.layer-box.active { border-color: var(--vp-c-brand-1); box-shadow: 0 0 0 2px var(--vp-c-brand-soft); }
|
|
|
|
|
|
.layer-box.controller { border-left-color: #10b981; }
|
|
|
|
|
|
.layer-box.service { border-left-color: #f59e0b; }
|
|
|
|
|
|
.layer-box.repository { border-left-color: #3b82f6; }
|
|
|
|
|
|
.layer-box.domain { border-left-color: #6b7280; }
|
|
|
|
|
|
|
|
|
|
|
|
.layer-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
|
|
|
|
|
|
.layer-name { font-weight: 600; font-size: 14px; color: var(--vp-c-text-1); }
|
2026-02-06 03:34:50 +08:00
|
|
|
|
.layer-badge {
|
2026-03-01 12:28:47 +08:00
|
|
|
|
padding: 1px 8px; border-radius: 10px; font-size: 11px;
|
|
|
|
|
|
background: var(--vp-c-bg-soft); color: var(--vp-c-text-3);
|
2026-02-06 03:34:50 +08:00
|
|
|
|
}
|
2026-03-01 12:28:47 +08:00
|
|
|
|
.layer-duty { font-size: 12px; color: var(--vp-c-text-2); }
|
2026-02-06 03:34:50 +08:00
|
|
|
|
|
|
|
|
|
|
.info-panel {
|
2026-03-01 12:28:47 +08:00
|
|
|
|
width: 300px; padding: 18px; border-radius: 10px;
|
|
|
|
|
|
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
|
|
|
|
|
|
position: sticky; top: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-title { font-weight: 600; font-size: 14px; color: var(--vp-c-text-1); margin-bottom: 10px; padding-bottom: 8px; border-bottom: 2px solid var(--vp-c-brand-1); }
|
|
|
|
|
|
.info-panel p { font-size: 13px; color: var(--vp-c-text-2); line-height: 1.6; margin: 0 0 12px; }
|
|
|
|
|
|
.info-analogy {
|
|
|
|
|
|
padding: 10px; border-radius: 6px; font-size: 12px; line-height: 1.5;
|
|
|
|
|
|
background: var(--vp-c-brand-soft); color: var(--vp-c-text-1);
|
|
|
|
|
|
border-left: 3px solid var(--vp-c-brand-1); margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-mistakes {
|
|
|
|
|
|
padding: 10px; border-radius: 6px; font-size: 12px; line-height: 1.5;
|
|
|
|
|
|
background: var(--vp-c-danger-soft); color: var(--vp-c-text-1);
|
|
|
|
|
|
border-left: 3px solid var(--vp-c-danger-1);
|
|
|
|
|
|
}
|
|
|
|
|
|
.info-mistakes strong { font-size: 12px; }
|
|
|
|
|
|
.info-mistakes ul { margin: 6px 0 0; padding-left: 16px; }
|
|
|
|
|
|
.info-mistakes li { margin: 3px 0; }
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.main { flex-direction: column; }
|
|
|
|
|
|
.info-panel { width: 100%; position: static; }
|
2026-02-06 03:34:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|