d35211071a
- standardize border-radius from 8px to 6px for consistent styling - adjust padding values from 1rem to 0.75rem for better visual hierarchy - remove redundant overflow-y properties for cleaner code
341 lines
7.7 KiB
Vue
341 lines
7.7 KiB
Vue
<template>
|
||
<div class="controller-layer-demo">
|
||
<div class="demo-header">
|
||
<h4>🎮 Controller 层:请求的"接待员"</h4>
|
||
<p class="subtitle">点击流程节点查看 Controller 如何接收和处理请求</p>
|
||
</div>
|
||
|
||
<div class="flow-container">
|
||
<!-- 请求发起 -->
|
||
<div class="flow-step">
|
||
<div class="step-icon">🌐</div>
|
||
<div class="step-content">
|
||
<div class="step-title">客户端发起请求</div>
|
||
<div class="step-code">
|
||
POST /api/users/register
|
||
Content-Type: application/json
|
||
{
|
||
"username": "张三",
|
||
"email": "zhangsan@example.com",
|
||
"password": "123456"
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="arrow-connector">⬇️ 请求到达</div>
|
||
|
||
<!-- Controller 接收 -->
|
||
<div
|
||
class="flow-step controller-step"
|
||
:class="{ active: showDetails === 'controller' }"
|
||
@click="toggleDetails('controller')"
|
||
>
|
||
<div class="step-icon">🎮</div>
|
||
<div class="step-content">
|
||
<div class="step-title">Controller 接收并解析请求</div>
|
||
<div class="step-code">
|
||
@RestController
|
||
@RequestMapping("/api/users")
|
||
public class UserController {
|
||
|
||
@PostMapping("/register")
|
||
public ResponseEntity<UserDTO> register(
|
||
@RequestBody @Valid UserRegisterRequest request
|
||
) {
|
||
// 调用 Service 处理业务
|
||
UserDTO user = userService.register(request);
|
||
return ResponseEntity.ok(user);
|
||
}
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="arrow-connector">⬇️ 参数校验 + 调用</div>
|
||
|
||
<!-- 校验逻辑 -->
|
||
<div
|
||
class="flow-step validation-step"
|
||
:class="{ active: showDetails === 'validation' }"
|
||
@click="toggleDetails('validation')"
|
||
>
|
||
<div class="step-icon">✅</div>
|
||
<div class="step-content">
|
||
<div class="step-title">参数校验(Controller 的职责之一)</div>
|
||
<div class="step-code">
|
||
public class UserRegisterRequest {
|
||
@NotBlank(message = "用户名不能为空")
|
||
@Size(min = 2, max = 20, message = "用户名长度2-20")
|
||
private String username;
|
||
|
||
@Email(message = "邮箱格式不正确")
|
||
private String email;
|
||
|
||
@Size(min = 6, message = "密码至少6位")
|
||
private String password;
|
||
}
|
||
</div>
|
||
<div v-if="showDetails === 'validation'" class="detail-panel">
|
||
<h5>为什么校验要放在 Controller?</h5>
|
||
<ul>
|
||
<li>🛡️ 第一道防线:尽早拦截非法请求</li>
|
||
<li>📦 减轻下游压力:Service 层可以假设数据已清洗</li>
|
||
<li>🔧 关注点分离:Service 专注于业务,不处理格式验证</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="arrow-connector">⬇️ 返回结果</div>
|
||
|
||
<!-- 响应返回 -->
|
||
<div class="flow-step">
|
||
<div class="step-icon">📤</div>
|
||
<div class="step-content">
|
||
<div class="step-title">Controller 封装响应返回给客户端</div>
|
||
<div class="step-code">
|
||
HTTP/1.1 200 OK
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"code": 200,
|
||
"message": "注册成功",
|
||
"data": {
|
||
"id": 10001,
|
||
"username": "张三",
|
||
"email": "zhangsan@example.com",
|
||
"createdAt": "2024-01-15T10:30:00Z"
|
||
}
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Controller 职责总结 -->
|
||
<div class="controller-summary">
|
||
<h5>🎯 Controller 的核心职责</h5>
|
||
<div class="duty-grid">
|
||
<div class="duty-item">
|
||
<div class="duty-icon">📡</div>
|
||
<div class="duty-title">接收请求</div>
|
||
<div class="duty-desc">映射 HTTP 请求到方法</div>
|
||
</div>
|
||
<div class="duty-item">
|
||
<div class="duty-icon">✅</div>
|
||
<div class="duty-title">参数校验</div>
|
||
<div class="duty-desc">基础格式和必填校验</div>
|
||
</div>
|
||
<div class="duty-item">
|
||
<div class="duty-icon">🔄</div>
|
||
<div class="duty-title">调用 Service</div>
|
||
<div class="duty-desc">将请求转发给业务层</div>
|
||
</div>
|
||
<div class="duty-item">
|
||
<div class="duty-icon">📦</div>
|
||
<div class="duty-title">封装响应</div>
|
||
<div class="duty-desc">统一响应格式返回</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
|
||
const showDetails = ref('')
|
||
|
||
const toggleDetails = (section) => {
|
||
showDetails.value = showDetails.value === section ? '' : section
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.controller-layer-demo {
|
||
padding: 24px;
|
||
background: linear-gradient(135deg, #f0f7ff 0%, #e6f0ff 100%);
|
||
border-radius: 12px;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
}
|
||
|
||
.demo-header {
|
||
text-align: center;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.demo-header h4 {
|
||
margin: 0 0 8px 0;
|
||
color: #1a1a2e;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.subtitle {
|
||
margin: 0;
|
||
color: #666;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.flow-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.flow-step {
|
||
display: flex;
|
||
gap: 16px;
|
||
padding: 16px;
|
||
background: white;
|
||
border-radius: 10px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.flow-step:hover {
|
||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.flow-step.active {
|
||
border: 2px solid #409eff;
|
||
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.2);
|
||
}
|
||
|
||
.controller-step {
|
||
border-left: 4px solid #67c23a;
|
||
}
|
||
|
||
.validation-step {
|
||
border-left: 4px solid #e6a23c;
|
||
}
|
||
|
||
.step-icon {
|
||
font-size: 24px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.step-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.step-title {
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 8px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.step-code {
|
||
background: #f8f9fa;
|
||
padding: 12px;
|
||
border-radius: 6px;
|
||
font-family: 'Monaco', 'Menlo', monospace;
|
||
font-size: 11px;
|
||
color: #333;
|
||
white-space: pre-wrap;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.arrow-connector {
|
||
text-align: center;
|
||
padding: 8px;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.detail-panel {
|
||
margin-top: 12px;
|
||
padding: 16px;
|
||
background: #f0f7ff;
|
||
border-radius: 6px;
|
||
border-left: 4px solid #409eff;
|
||
}
|
||
|
||
.detail-panel h5 {
|
||
margin: 0 0 12px 0;
|
||
color: #1a1a2e;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.detail-panel ul {
|
||
margin: 0;
|
||
padding-left: 20px;
|
||
}
|
||
|
||
.detail-panel li {
|
||
margin: 6px 0;
|
||
color: #606266;
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.controller-summary {
|
||
margin-top: 24px;
|
||
padding: 20px;
|
||
background: white;
|
||
border-radius: 10px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.controller-summary h5 {
|
||
margin: 0 0 16px 0;
|
||
color: #1a1a2e;
|
||
font-size: 15px;
|
||
text-align: center;
|
||
}
|
||
|
||
.duty-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 12px;
|
||
}
|
||
|
||
.duty-item {
|
||
text-align: center;
|
||
padding: 16px 12px;
|
||
background: #f8f9fa;
|
||
border-radius: 6px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.duty-item:hover {
|
||
background: #e6f7ff;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.duty-icon {
|
||
font-size: 28px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.duty-title {
|
||
font-weight: 600;
|
||
color: #303133;
|
||
font-size: 13px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.duty-desc {
|
||
color: #909399;
|
||
font-size: 11px;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.duty-grid {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
|
||
.flow-step {
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.step-content {
|
||
width: 100%;
|
||
}
|
||
}
|
||
</style>
|