feat: update documentation and component demos for backend layered architecture

- Add new LanguageScopeDemo component for backend languages overview
- Refactor and simplify existing demo components (ControllerLayerDemo, DtoFlowDemo, DependencyDirectionDemo)
- Update .gitignore to exclude .claude/skills directory
- Modify backend-related sections in documentation from "后端与全栈" to "后端开发"
- Add new backend layered architecture demo components (CleanArchitectureDemo, DependencyDirectionDemo)
- Improve documentation structure and content for stage-3 core skills
- Fix component initialization timing in CompileVsInterpretDemo and RateLimiterDemo
- Add design style prompt reference in frontend documentation
This commit is contained in:
sanbuphy
2026-03-01 12:28:47 +08:00
parent d8eb93663d
commit dc8b5773f1
22 changed files with 2660 additions and 5288 deletions
+1
View File
@@ -21,3 +21,4 @@ docs/.vitepress/.temp/*
.vitepress/cache/*
.vitepress/cache/*
docs/archived-components.md
.claude/skills/*
+1 -1
View File
@@ -275,7 +275,7 @@ Easy-Vibe 通过以下几个阶段,带你从 0 到 1:
| [前端三:参考 UI 设计规范与多产品 UI 设计](docs/zh-cn/stage-2/frontend/2.3-multi-product-ui/) | 围绕统一主视觉扩展多产品界面,练习系统化设计能力 | 🚧 |
| [前端四:一起做霍格沃茨画像](docs/zh-cn/stage-2/frontend/2.4-hogwarts-portraits/chapter4-lets-build-hogwarts-portraits.md) | 从 0 到 1 做出接入 AI 能力的前端应用,串联设计与开发 | ✅ |
#### 后端与全栈部分
#### 后端开发部分
| 章节 | 关键内容 | 状态 |
| :-------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------ | :--- |
+83 -15
View File
@@ -372,12 +372,32 @@ const stage3SidebarEn = [
collapsed: false,
items: [
{
text: 'Advanced 1: MCP & Claude Code Skills',
link: '/zh-cn/stage-3/core-skills/3.1-mcp-claude-code-skills/'
text: 'Claude Code 快速上手核心指南',
link: '/zh-cn/stage-3/core-skills/basics/'
},
{
text: 'Advanced 2: Long Running Tasks',
link: '/zh-cn/stage-3/core-skills/3.2-long-running-tasks/'
text: 'Claude Code MCP 完全指南',
link: '/zh-cn/stage-3/core-skills/mcp/'
},
{
text: 'Claude Code Skills 完全指南',
link: '/zh-cn/stage-3/core-skills/skills/'
},
{
text: 'Long Running Tasks',
link: '/zh-cn/stage-3/core-skills/long-running-tasks/'
},
{
text: 'Claude Agent Teams 完全指南',
link: '/zh-cn/stage-3/core-skills/agent-teams/'
},
{
text: 'Claude Code Superpowers 工程级开发',
link: '/zh-cn/stage-3/core-skills/superpowers/'
},
{
text: 'Claude Code 工作流最佳实践',
link: '/zh-cn/stage-3/core-skills/workflow/'
}
]
},
@@ -475,6 +495,30 @@ const appendixSidebarEn = [
{
text: 'Type Systems Intro',
link: '/zh-cn/appendix/1-computer-fundamentals/type-systems'
},
{
text: 'Linking & Loading',
link: '/zh-cn/appendix/1-computer-fundamentals/linking-loading'
},
{
text: 'Assembly Basics',
link: '/zh-cn/appendix/1-computer-fundamentals/assembly-basics'
},
{
text: 'Memory Hierarchy',
link: '/zh-cn/appendix/1-computer-fundamentals/memory-hierarchy'
},
{
text: 'Processor Architecture',
link: '/zh-cn/appendix/1-computer-fundamentals/processor-architecture'
},
{
text: 'System I/O',
link: '/zh-cn/appendix/1-computer-fundamentals/system-io'
},
{
text: 'Socket Programming',
link: '/zh-cn/appendix/1-computer-fundamentals/socket-programming'
}
]
},
@@ -1092,24 +1136,28 @@ export default defineConfig({
text: '参考 UI 设计规范与多产品 UI 设计',
link: '/zh-cn/stage-2/frontend/2.3-multi-product-ui/'
},
{
text: '用 LLM 和 Skills 让界面变好看',
link: '/zh-cn/stage-2/frontend/2.4-llm-skills-beautiful/'
},
{
text: '霍格沃茨的画像们:SVG 交互动画',
link: '/zh-cn/stage-2/frontend/2.5-hogwarts-portraits/'
},
{
text: '从设计原型到项目代码',
link: '/zh-cn/stage-2/frontend/2.5-design-to-code/'
link: '/zh-cn/stage-2/frontend/2.6-design-to-code/'
},
{
text: '使用现代组件库更新你的界面',
link: '/zh-cn/stage-2/frontend/2.6-modern-component-library/'
link: '/zh-cn/stage-2/frontend/2.7-modern-component-library/'
}
]
},
{
text: '后端与全栈',
text: '后端开发',
collapsed: false,
items: [
{
text: '什么是 API',
link: '/zh-cn/stage-2/backend/2.1-what-is-api/'
},
{
text: '从数据库到 Supabase',
link: '/zh-cn/stage-2/backend/2.2-database-supabase/'
@@ -1167,16 +1215,36 @@ export default defineConfig({
],
'/zh-cn/stage-3/': [
{
text: 'claudecode 深入浅出',
text: 'Claude Code 深入浅出',
collapsed: false,
items: [
{
text: 'MCP 与 Claude Code Skills',
link: '/zh-cn/stage-3/core-skills/3.1-mcp-claude-code-skills/'
text: 'Claude Code 快速上手核心指南',
link: '/zh-cn/stage-3/core-skills/basics/'
},
{
text: 'Claude Code MCP 完全指南',
link: '/zh-cn/stage-3/core-skills/mcp/'
},
{
text: 'Claude Code Skills 完全指南',
link: '/zh-cn/stage-3/core-skills/skills/'
},
{
text: '如何让 Coding Tools 长时间工作',
link: '/zh-cn/stage-3/core-skills/3.2-long-running-tasks/'
link: '/zh-cn/stage-3/core-skills/long-running-tasks/'
},
{
text: 'Claude Agent Teams 完全指南',
link: '/zh-cn/stage-3/core-skills/agent-teams/'
},
{
text: 'Claude Code Superpowers 工程级开发',
link: '/zh-cn/stage-3/core-skills/superpowers/'
},
{
text: 'Claude Code 工作流最佳实践',
link: '/zh-cn/stage-3/core-skills/workflow/'
}
]
},
@@ -0,0 +1,201 @@
<template>
<div class="lang-scope">
<div class="nav-bar">
<button class="arrow" :disabled="current === 0" @click="current--"></button>
<div class="tabs">
<button
v-for="(lang, i) in langs"
:key="lang.id"
class="tab"
:class="{ active: current === i }"
@click="current = i"
>{{ lang.icon }} {{ lang.name }}</button>
</div>
<button class="arrow" :disabled="current === langs.length - 1" @click="current++"></button>
</div>
<div class="card">
<div class="card-header">
<span class="lang-icon">{{ langs[current].icon }}</span>
<div>
<div class="lang-name">{{ langs[current].name }}</div>
<div class="lang-desc">{{ langs[current].tagline }}</div>
</div>
<span class="dir-count">{{ langs[current].dirs.length }} 个方向</span>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:18%">应用方向</th>
<th style="width:46%">细分示例与说明</th>
<th style="width:36%">典型应用 / 程序</th>
</tr>
</thead>
<tbody>
<tr v-for="d in langs[current].dirs" :key="d.dir">
<td class="dir-cell">{{ d.dir }}</td>
<td>{{ d.detail }}</td>
<td class="apps-cell"><span v-for="a in d.apps" :key="a" class="app-tag">{{ a }}</span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const current = ref(0)
const langs = [
{
id: 'java', icon: '☕', name: 'Java',
tagline: '企业级常青树 · JVM 生态 · 强类型 · 大数据基石',
dirs: [
{ dir: '企业级 Web 后端', detail: 'Spring Boot / Spring Cloud 微服务;MyBatis/JPA 数据访问;Spring Security 认证授权', apps: ['淘宝核心系统', 'Spring Boot 项目', '银行网银系统'] },
{ dir: '大数据处理', detail: 'Hadoop MapReduce 批处理;Spark 流/批计算;Flink 实时流处理;Hive 数据仓库', apps: ['Hadoop', 'Spark', 'Flink', 'Hive'] },
{ dir: '中间件开发', detail: '消息队列(Kafka/RocketMQ);RPC 框架(Dubbo);注册中心(Nacos/Zookeeper', apps: ['Kafka', 'RocketMQ', 'Dubbo', 'Nacos'] },
{ dir: '搜索引擎', detail: 'Elasticsearch 全文检索;Lucene 底层索引;Solr 企业搜索', apps: ['Elasticsearch', 'Lucene', 'Solr'] },
{ dir: '金融交易系统', detail: '低延迟撮合引擎;风控规则引擎;清算结算系统', apps: ['LMAX Exchange', '蚂蚁金服核心'] },
{ dir: 'Android 应用', detail: 'Android SDK 原生开发;Jetpack 组件库;与 Kotlin 混合开发', apps: ['企业内部 App', 'Android SDK'] },
{ dir: '构建与 DevOps', detail: 'Maven/Gradle 构建;Jenkins CI/CDSonarQube 代码质量', apps: ['Maven', 'Gradle', 'Jenkins'] },
{ dir: '桌面应用', detail: 'JavaFX 桌面 GUISwing 遗留系统;跨平台工具', apps: ['IntelliJ IDEA', 'Eclipse', 'DBeaver'] }
]
},
{
id: 'nodejs', icon: '💚', name: 'Node.js',
tagline: 'JavaScript 全栈 · 事件驱动 · npm 生态最大 · 实时通信',
dirs: [
{ dir: 'Web API 后端', detail: 'Express/Koa/Fastify REST APINestJS 企业级框架;tRPC 类型安全', apps: ['NestJS', 'Express API', 'Strapi CMS'] },
{ dir: '全栈框架', detail: 'Next.js App RouterReact SSR);Nuxt 3Vue SSR);RemixAstro', apps: ['Next.js', 'Nuxt', 'Remix', 'T3 Stack'] },
{ dir: '实时通信', detail: 'Socket.io WebSocketYjs/Automerge CRDT 协同编辑;WebRTC 信令', apps: ['协作文档', '实时白板', '聊天室'] },
{ dir: 'Serverless', detail: 'Vercel Edge FunctionsCloudflare WorkersAWS Lambda Node', apps: ['Vercel Serverless', 'Cloudflare Worker'] },
{ dir: 'CLI 工具', detail: 'Commander/Yargs 参数解析;Ink 终端 UInpx 分发', apps: ['create-react-app', 'Vercel CLI', 'eslint'] },
{ dir: 'Electron 桌面', detail: 'Electron + React/Vue 跨平台桌面;electron-builder 打包', apps: ['VS Code', 'Slack', 'Notion', 'Discord'] },
{ dir: '浏览器/编辑器插件', detail: 'Chrome Extension MV3VS Code ExtensionObsidian Plugin', apps: ['uBlock Origin', '沉浸式翻译', 'GitLens'] },
{ dir: 'Bot 与自动化', detail: 'TelegrafTelegram Bot);discord.jsSlack Bolt', apps: ['grammY Bot', 'discord.js Bot'] }
]
},
{
id: 'go', icon: '🐹', name: 'Go',
tagline: '云原生之王 · 天然高并发 · 单二进制分发 · DevOps 基石',
dirs: [
{ dir: '高并发 Web API', detail: 'Gin/Echo/Fiber 框架 REST API;标准库 net/httpgoroutine 天然并发', apps: ['Gin API', 'Echo 微服务', 'Fiber API'] },
{ dir: '微服务架构', detail: 'gRPC + Protobuf 通信;go-zero/Kratos 框架;服务注册/链路追踪', apps: ['gRPC 微服务', 'go-zero', 'Kratos'] },
{ dir: '云原生基础设施', detail: 'Docker/K8s/Terraform/Prometheus/etcd 全是 Go;自研 K8s Operator', apps: ['Docker', 'Kubernetes', 'Terraform', 'Prometheus'] },
{ dir: 'CLI 命令行工具', detail: 'Cobra 框架;Bubble Tea TUI;编译单文件跨平台分发', apps: ['kubectl', 'gh CLI', 'lazygit', 'fzf'] },
{ dir: '网络代理与中间件', detail: '反向代理/负载均衡;API 网关;VPN/内网穿透;DNS 服务', apps: ['Caddy', 'Traefik', 'frp', 'CoreDNS'] },
{ dir: '分布式存储', detail: '分布式 KV 存储;对象存储;时序数据库', apps: ['etcd', 'MinIO', 'TiKV', 'InfluxDB'] },
{ dir: '区块链', detail: '以太坊客户端;Hyperledger Fabric;共识算法实现', apps: ['go-ethereum', 'Hyperledger Fabric'] },
{ dir: '监控与可观测', detail: 'Prometheus 指标采集;Grafana Agent;日志收集', apps: ['Prometheus', 'Grafana Agent', 'Loki'] }
]
},
{
id: 'rust', icon: '🦀', name: 'Rust',
tagline: '内存安全 · 零成本抽象 · C++ 现代替代 · 增长最快的系统语言',
dirs: [
{ dir: 'Tauri 桌面应用', detail: 'Tauri 2.0 替代 Electron(体积小 10 倍+);前端 React/Vue + 后端 Rust', apps: ['Tauri App', 'Spacedrive', 'AppFlowy'] },
{ dir: 'WebAssembly 模块', detail: 'Rust → WASM 高性能计算(图像/PDF/加密);Web 端编解码', apps: ['Figma 渲染引擎', 'SWC', 'wasm-pack'] },
{ dir: 'CLI 命令行工具', detail: 'ripgrep/fd/bat/exa 等现代 CLI;编译为单二进制零依赖', apps: ['ripgrep', 'fd', 'bat', 'starship', 'delta'] },
{ dir: '操作系统开发', detail: 'Redox OS 微内核;Linux 6.1+ Rust 内核模块;嵌入式 RTOS', apps: ['Redox OS', 'Linux Rust 模块', 'Tock OS'] },
{ dir: '嵌入式开发', detail: 'embedded-rust 在 STM32/ESP32 固件;RTIC 实时并发框架', apps: ['embassy-rs', 'RTIC 项目', 'ESP-RS'] },
{ dir: 'Serverless / 边缘', detail: 'Cloudflare Workers Rust→WASMFastly Compute@Edge;冷启动极快', apps: ['Cloudflare Workers', 'Fermyon Spin', 'WasmEdge'] },
{ dir: '高性能网络工具', detail: '网络代理;反向代理/负载均衡;VPN;内网穿透;DNS', apps: ['Pingora', 'Linkerd2-proxy', 'Hickory DNS'] },
{ dir: '区块链开发', detail: 'Solana 链上程序;Substrate 框架(Polkadot);零知识证明', apps: ['Solana', 'Substrate', 'StarkNet'] },
{ dir: 'Web 后端服务', detail: 'Actix-web / Axum 高性能 APIgRPC;低延迟金融/游戏后端', apps: ['Axum API', 'Actix-web', 'Tonic gRPC'] }
]
},
{
id: 'csharp', icon: '🟣', name: 'C#',
tagline: '.NET 生态 · 企业级 · Unity 游戏 · 跨平台',
dirs: [
{ dir: '企业级 Web 后端', detail: 'ASP.NET Core Web APIEntity Framework ORMSignalR 实时通信', apps: ['Stack Overflow', 'ASP.NET 项目'] },
{ dir: 'Unity 游戏开发', detail: 'Unity 引擎 C# 脚本;2D/3D 游戏;AR/VR 应用;游戏工具', apps: ['Unity 游戏', 'Pokemon GO', 'Beat Saber'] },
{ dir: 'Windows 桌面', detail: 'WPF/WinUI 3 桌面 GUIWinForms 遗留系统;MAUI 跨平台', apps: ['Visual Studio', 'Paint.NET', 'Windows Terminal'] },
{ dir: 'Azure 云服务', detail: 'Azure Functions ServerlessAzure SDK;微服务(Dapr', apps: ['Azure Functions', 'Dapr', 'Orleans'] },
{ dir: '微服务架构', detail: '.NET Aspire 云原生;gRPC 通信;MassTransit 消息总线', apps: ['.NET Aspire', 'MassTransit', 'CAP'] },
{ dir: 'Blazor Web 前端', detail: 'Blazor Server/WASM 用 C# 写前端;替代 JavaScript', apps: ['Blazor 项目', 'Radzen 组件库'] }
]
},
{
id: 'kotlin', icon: '🟠', name: 'Kotlin',
tagline: '现代 JVM 语言 · Android 官方 · 空安全 · 协程',
dirs: [
{ dir: 'Android 应用', detail: 'Jetpack Compose 声明式 UIGoogle 官方推荐语言', apps: ['Google App', 'Coursera', 'Pinterest'] },
{ dir: 'JVM 后端服务', detail: 'Ktor 轻量框架;Spring Boot Kotlin 支持;协程异步', apps: ['Ktor 服务', 'Spring Boot Kotlin'] },
{ dir: '跨平台开发', detail: 'Kotlin MultiplatformKMP)共享业务逻辑 iOS/Android/Web', apps: ['KMP 项目', 'Netflix (部分)'] },
{ dir: '服务端脚本', detail: 'Kotlin Script (.kts)Gradle 构建脚本(build.gradle.kts', apps: ['Gradle Kotlin DSL', 'kscript'] },
{ dir: '数据处理', detail: 'Kotlin DataFrame;与 Spark/Flink Java 生态互操作', apps: ['Kotlin DataFrame', 'Spark Kotlin'] }
]
},
{
id: 'scala', icon: '🔴', name: 'Scala',
tagline: '大数据 JVM 之王 · 函数式+面向对象 · Spark 生态',
dirs: [
{ dir: '大数据处理', detail: 'Spark 批/流计算;Flink Scala API;数据管道 ETL', apps: ['Apache Spark', 'Apache Flink', 'Databricks'] },
{ dir: '分布式系统', detail: 'Akka Actor 模型;Akka Cluster 集群;Akka Streams 流处理', apps: ['Akka 项目', 'Lightbend 平台'] },
{ dir: '金融系统', detail: '风险分析引擎;量化交易策略;复杂计算模型', apps: ['高盛交易系统', 'Morgan Stanley'] },
{ dir: 'Web 后端', detail: 'Play Framework 异步 WebScala.js 前端;http4s 函数式', apps: ['Play Framework', 'Twitter 后端', 'LinkedIn'] },
{ dir: '消息系统', detail: 'Kafka Streams 流处理;Kafka Connect 数据集成', apps: ['Apache Kafka', 'Kafka Streams'] }
]
},
{
id: 'swift', icon: '🍎', name: 'Swift',
tagline: 'Apple 官方语言 · 类型安全 · 高性能 · iOS/macOS 生态',
dirs: [
{ dir: 'iOS 应用', detail: 'SwiftUI / UIKit 原生开发;Combine 响应式;WidgetKit 小组件', apps: ['所有 iOS App', 'Apple 全家桶'] },
{ dir: 'macOS 应用', detail: 'AppKit / SwiftUI 桌面;菜单栏工具;系统扩展', apps: ['Xcode', 'Swift Playgrounds'] },
{ dir: 'Web 后端', detail: 'Vapor / Hummingbird 框架;SwiftNIO 网络层', apps: ['Vapor API', 'Hummingbird 服务'] },
{ dir: '跨平台移动', detail: 'Swift on Server + iOS 共享模型层;Swift for Android(实验)', apps: ['LinkedIn (部分)', 'Airbnb (部分)'] },
{ dir: '系统编程', detail: '与 C/Obj-C 互操作;底层框架开发;驱动/内核扩展', apps: ['Apple 系统框架', 'Swift 编译器'] }
]
},
{
id: 'ruby', icon: '💎', name: 'Ruby',
tagline: '开发者幸福 · Rails 快速开发 · 元编程 · 优雅语法',
dirs: [
{ dir: 'Web 全栈', detail: 'Ruby on Rails MVCHotwire/Turbo 现代前端;Action Cable 实时', apps: ['GitHub', 'Shopify', 'Basecamp'] },
{ dir: '快速原型 / MVP', detail: 'Rails scaffold 脚手架;ActiveRecord ORM;约定优于配置', apps: ['Airbnb 早期', 'Twitter 早期'] },
{ dir: 'API 后端', detail: 'Grape / Rails API 模式;GraphQL RubySidekiq 后台任务', apps: ['Stripe API', 'GitLab'] },
{ dir: 'DevOps 工具', detail: 'Chef/Puppet 配置管理;Vagrant 虚拟化;Homebrew 包管理', apps: ['Homebrew', 'Vagrant', 'Chef'] },
{ dir: '脚本与自动化', detail: 'Rake 任务;数据迁移脚本;文本处理', apps: ['Fastlane', 'CocoaPods', 'Jekyll'] }
]
},
{
id: 'wasm', icon: '🔮', name: 'WebAssembly',
tagline: '浏览器二进制格式 · 多语言编译目标 · 沙箱安全 · 接近原生性能',
dirs: [
{ dir: '浏览器高性能计算', detail: '图像/视频处理;PDF 渲染;加密解密;科学计算', apps: ['Figma', 'Google Earth', 'Photoshop Web'] },
{ dir: '游戏引擎 Web 化', detail: 'Unity/Godot/Unreal 编译到 WebWebGL + WASM 渲染', apps: ['Unity WebGL', 'Godot Web', 'itch.io 游戏'] },
{ dir: '开发工具链', detail: 'SWC/esbuild 编译器;SQLite WASM;语言 Playground', apps: ['SWC', 'esbuild', 'SQLite WASM'] },
{ dir: 'Serverless / 边缘', detail: 'Cloudflare Workers WASMFermyon SpinFastly Compute', apps: ['Cloudflare Workers', 'Fermyon Spin'] },
{ dir: '插件沙箱', detail: 'Envoy WASM FilterFigma 插件;安全隔离执行第三方代码', apps: ['Envoy Proxy', 'Figma 插件', 'Extism'] }
]
},
]
</script>
<style scoped>
.lang-scope { border: 1px solid var(--vp-c-divider); border-radius: 8px; background: var(--vp-c-bg-soft); padding: 0.75rem; margin: 1rem 0; }
.nav-bar { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; }
.arrow { background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); border-radius: 6px; padding: 0.25rem 0.5rem; cursor: pointer; font-size: 0.8rem; }
.arrow:disabled { opacity: 0.3; cursor: not-allowed; }
.tabs { display: flex; gap: 0.25rem; overflow-x: auto; flex: 1; }
.tab { white-space: nowrap; padding: 0.25rem 0.5rem; border: 1px solid transparent; border-radius: 6px; background: none; cursor: pointer; font-size: 0.75rem; color: var(--vp-c-text-2); transition: all 0.2s; }
.tab:hover { background: var(--vp-c-bg); }
.tab.active { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
.card { background: var(--vp-c-bg); border-radius: 8px; overflow: hidden; }
.card-header { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--vp-c-divider); }
.lang-icon { font-size: 1.5rem; }
.lang-name { font-weight: 700; font-size: 0.95rem; }
.lang-desc { font-size: 0.75rem; color: var(--vp-c-text-2); }
.dir-count { margin-left: auto; font-size: 0.75rem; color: var(--vp-c-text-3); white-space: nowrap; }
.table-wrap { overflow-x: auto; max-height: 320px; overflow-y: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
thead { position: sticky; top: 0; background: var(--vp-c-bg); z-index: 1; }
th { text-align: left; padding: 0.4rem 0.6rem; border-bottom: 2px solid var(--vp-c-divider); font-size: 0.75rem; color: var(--vp-c-text-2); }
td { padding: 0.4rem 0.6rem; border-bottom: 1px solid var(--vp-c-divider); vertical-align: top; line-height: 1.5; }
.dir-cell { font-weight: 600; white-space: nowrap; color: var(--vp-c-brand-1); }
.apps-cell { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.app-tag { display: inline-block; padding: 0.1rem 0.4rem; background: var(--vp-c-bg-soft); border-radius: 4px; font-size: 0.7rem; white-space: nowrap; }
</style>
@@ -1,228 +1,78 @@
<template>
<div class="clean-architecture-demo">
<div class="demo-header">
<h4>🏗 整洁架构Clean Architecture与分层架构</h4>
<p class="subtitle">
分层架构是整洁架构的基础理解两者的关系有助于构建更灵活的系统
</p>
<div class="clean-arch-demo">
<div class="header">
<div class="title">整洁架构与分层架构对比</div>
<div class="subtitle">分层架构是整洁架构的基础理解两者关系有助于构建更灵活的系统</div>
</div>
<!-- 架构对比 -->
<div class="architecture-comparison">
<div class="comparison-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-btn', { active: currentTab === tab.id }]"
@click="currentTab = tab.id"
>
{{ tab.name }}
</button>
<div class="tabs">
<button
v-for="t in tabs" :key="t.id"
:class="['tab', { active: current === t.id }]"
@click="current = t.id"
>{{ t.name }}</button>
</div>
<div v-if="current === 'layered'" class="panel">
<div class="arch-layers">
<div v-for="l in layeredLayers" :key="l.name" :class="['arch-layer', l.cls]">
<strong>{{ l.name }}</strong> <span>{{ l.desc }}</span>
</div>
</div>
<div class="traits">
<strong>传统分层架构特点</strong>
<ul>
<li>垂直依赖上层直接依赖下层</li>
<li>简单直观结构清晰易于理解</li>
<li>适合中小型项目快速开发上手简单</li>
<li>潜在问题底层变更可能影响上层</li>
</ul>
</div>
</div>
<div class="comparison-content">
<!-- 传统分层架构 -->
<div
v-if="currentTab === 'layered'"
class="tab-panel"
>
<div class="arch-diagram layered">
<div class="layer-box controller">
<div class="layer-title">
Controller
</div>
<div class="layer-desc">
接收请求参数校验
</div>
</div>
<div class="arrow down">
依赖
</div>
<div class="layer-box service">
<div class="layer-title">
Service
</div>
<div class="layer-desc">
业务逻辑事务管理
</div>
</div>
<div class="arrow down">
依赖
</div>
<div class="layer-box repository">
<div class="layer-title">
Repository
</div>
<div class="layer-desc">
数据访问ORM 映射
</div>
</div>
<div class="arrow down">
依赖
</div>
<div class="layer-box domain">
<div class="layer-title">
Domain
</div>
<div class="layer-desc">
实体定义业务规则
</div>
</div>
</div>
<div class="arch-characteristics">
<h5>📌 传统分层架构特点</h5>
<ul>
<li><strong>垂直依赖</strong>上层直接依赖下层依赖方向从上到下</li>
<li><strong>简单直观</strong>结构清晰易于理解和实现</li>
<li><strong>适合中小型项目</strong>快速开发上手简单</li>
<li><strong>潜在问题</strong>底层变更可能影响上层循环依赖风险</li>
</ul>
</div>
<div v-else-if="current === 'clean'" class="panel">
<div class="clean-layers">
<div v-for="l in cleanLayers" :key="l.name" :class="['arch-layer', l.cls]">
<strong>{{ l.name }}</strong> <span>{{ l.items }}</span>
</div>
</div>
<div class="dep-rule">依赖方向外层 内层内层不知道外层的存在</div>
<div class="traits">
<strong>整洁架构特点</strong>
<ul>
<li>依赖倒置依赖方向从外到内通过接口隔离</li>
<li>领域为核心业务逻辑位于中心独立于框架</li>
<li>可测试性强核心业务可脱离框架单元测试</li>
<li>技术无关可轻松切换数据库框架等</li>
</ul>
</div>
</div>
<!-- 整洁架构 -->
<div
v-else-if="currentTab === 'clean'"
class="tab-panel"
>
<div class="arch-diagram clean">
<div class="clean-layers">
<div class="clean-layer framework">
<div class="layer-name">
框架与驱动层
</div>
<div class="layer-items">
Web / DB / UI / 外部接口
</div>
</div>
<div class="clean-layer interface">
<div class="layer-name">
接口适配层
</div>
<div class="layer-items">
Controller / Gateway / Presenter
</div>
</div>
<div class="clean-layer application">
<div class="layer-name">
应用层
</div>
<div class="layer-items">
Service / UseCase / DTO
</div>
</div>
<div class="clean-layer domain">
<div class="layer-name">
领域层核心
</div>
<div class="layer-items">
Entity / ValueObject / DomainService
</div>
</div>
</div>
<div class="dependency-rule">
<div class="rule-arrow">
<span class="arrow-line" />
<span class="arrow-head"> 依赖方向</span>
</div>
<div class="rule-text">
外层依赖内层内层不依赖外层
</div>
</div>
</div>
<div class="arch-characteristics">
<h5>📌 整洁架构特点</h5>
<ul>
<li><strong>依赖倒置</strong>依赖方向从外到内通过接口隔离</li>
<li><strong>领域为核心</strong>业务逻辑位于中心独立于框架</li>
<li><strong>可测试性强</strong>核心业务可脱离框架进行单元测试</li>
<li><strong>技术无关</strong>可轻松切换数据库框架等外部技术</li>
</ul>
</div>
<div v-else class="panel">
<table>
<thead><tr><th>特性</th><th>传统分层</th><th>整洁架构</th></tr></thead>
<tbody>
<tr v-for="r in compareRows" :key="r.feature">
<td>{{ r.feature }}</td><td>{{ r.layered }}</td><td>{{ r.clean }}</td>
</tr>
</tbody>
</table>
<div class="rec-grid">
<div class="rec-card">
<strong>选择传统分层当...</strong>
<ul>
<li>项目规模较小业务简单</li>
<li>团队对 DDD 不熟悉</li>
<li>需要快速上线验证市场</li>
</ul>
</div>
<!-- 对比总结 -->
<div
v-else
class="tab-panel"
>
<div class="comparison-table">
<table>
<thead>
<tr>
<th>特性</th>
<th>传统分层架构</th>
<th>整洁架构</th>
</tr>
</thead>
<tbody>
<tr>
<td>依赖方向</td>
<td>从上到下</td>
<td>从外到内</td>
</tr>
<tr>
<td>核心业务位置</td>
<td>Service </td>
<td>Domain 中心</td>
</tr>
<tr>
<td>框架依赖</td>
<td>较深 Spring</td>
<td>较浅通过接口隔离</td>
</tr>
<tr>
<td>可测试性</td>
<td>需要集成测试</td>
<td>核心可单元测试</td>
</tr>
<tr>
<td>学习曲线</td>
<td>平缓</td>
<td>较陡</td>
</tr>
<tr>
<td>适用场景</td>
<td>中小型项目快速迭代</td>
<td>大型复杂业务长期维护</td>
</tr>
</tbody>
</table>
</div>
<div class="recommendation">
<h5>💡 选型建议</h5>
<div class="rec-grid">
<div class="rec-card">
<div class="rec-title">
选择传统分层架构当...
</div>
<ul>
<li>项目规模较小业务相对简单</li>
<li>团队对 DDD 不熟悉</li>
<li>需要快速上线验证市场</li>
<li>技术栈相对固定</li>
</ul>
</div>
<div class="rec-card recommended">
<div class="rec-title">
选择整洁架构当...
</div>
<ul>
<li>业务复杂领域模型丰富</li>
<li>需要长期维护和演进</li>
<li>需要频繁切换技术栈</li>
<li>团队有较强的设计能力</li>
</ul>
<div class="rec-badge">
推荐
</div>
</div>
</div>
</div>
<div class="rec-card recommended">
<strong>选择整洁架构当...</strong>
<ul>
<li>业务复杂领域模型丰富</li>
<li>需要长期维护和演进</li>
<li>需要频繁切换技术栈</li>
</ul>
</div>
</div>
</div>
@@ -232,358 +82,91 @@
<script setup>
import { ref } from 'vue'
const currentTab = ref('layered')
const current = ref('layered')
const tabs = [
{ id: 'layered', name: '传统分层' },
{ id: 'clean', name: '整洁架构' },
{ id: 'comparison', name: '对比总结' }
{ id: 'compare', name: '对比总结' }
]
const layeredLayers = [
{ name: 'Controller 层', desc: '接收请求、参数校验', cls: 'green' },
{ name: 'Service 层', desc: '业务逻辑、事务管理', cls: 'orange' },
{ name: 'Repository 层', desc: '数据访问、ORM 映射', cls: 'blue' },
{ name: 'Domain 层', desc: '实体定义、业务规则', cls: 'teal' }
]
const cleanLayers = [
{ name: '领域层(核心)', items: 'Entity / ValueObject / DomainService', cls: 'teal' },
{ name: '应用层', items: 'Service / UseCase / DTO', cls: 'orange' },
{ name: '接口适配层', items: 'Controller / Gateway / Presenter', cls: 'blue' },
{ name: '框架与驱动层', items: 'Web / DB / UI / 外部接口', cls: 'gray' }
]
const compareRows = [
{ feature: '依赖方向', layered: '从上到下', clean: '从外到内' },
{ feature: '核心业务位置', layered: 'Service 层', clean: 'Domain 层(中心)' },
{ feature: '框架依赖', layered: '较深', clean: '较浅(接口隔离)' },
{ feature: '可测试性', layered: '需要集成测试', clean: '核心可单元测试' },
{ feature: '学习曲线', layered: '平缓', clean: '较陡' },
{ feature: '适用场景', layered: '中小型、快速迭代', clean: '大型复杂、长期维护' }
]
</script>
<style scoped>
.clean-architecture-demo {
padding: 24px;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
.clean-arch-demo { padding: 20px; background: var(--vp-c-bg-soft); border-radius: 12px; }
.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; }
.tabs { display: flex; gap: 8px; margin-bottom: 16px; }
.tab {
padding: 7px 16px; border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg);
border-radius: 6px; cursor: pointer; font-size: 13px; color: var(--vp-c-text-2); transition: all .2s;
}
.tab:hover { color: var(--vp-c-brand-1); border-color: var(--vp-c-brand-1); }
.tab.active { background: var(--vp-c-brand-1); border-color: var(--vp-c-brand-1); color: #fff; }
.panel {
padding: 18px; border-radius: 10px;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
}
.demo-header {
text-align: center;
margin-bottom: 24px;
.arch-layers, .clean-layers { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
.arch-layer {
padding: 12px 14px; border-radius: 6px;
background: var(--vp-c-bg-soft); border-left: 3px solid var(--vp-c-divider);
font-size: 13px; color: var(--vp-c-text-2);
}
.arch-layer strong { color: var(--vp-c-text-1); margin-right: 8px; }
.arch-layer.green { border-left-color: #10b981; }
.arch-layer.orange { border-left-color: #f59e0b; }
.arch-layer.blue { border-left-color: #3b82f6; }
.arch-layer.teal { border-left-color: #14b8a6; }
.arch-layer.gray { border-left-color: #6b7280; }
.dep-rule {
text-align: center; padding: 10px; margin-bottom: 16px; border-radius: 6px;
border: 2px dashed var(--vp-c-brand-1); font-size: 13px; color: var(--vp-c-brand-1); font-weight: 500;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
}
.traits { padding: 14px; border-radius: 6px; background: var(--vp-c-bg-soft); font-size: 13px; }
.traits strong { color: var(--vp-c-text-1); }
.traits ul { margin: 8px 0 0; padding-left: 18px; }
.traits li { margin: 4px 0; color: var(--vp-c-text-2); line-height: 1.5; }
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
table { width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 16px; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid var(--vp-c-divider); color: var(--vp-c-text-2); }
th { background: var(--vp-c-bg-soft); font-weight: 600; color: var(--vp-c-text-1); }
.architecture-comparison {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.comparison-tabs {
display: flex;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
}
.tab-btn {
padding: 12px 24px;
border: none;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.tab-btn:hover {
color: #409eff;
}
.tab-btn.active {
color: #409eff;
background: white;
font-weight: 500;
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: #409eff;
}
.comparison-content {
padding: 20px;
}
.tab-panel {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Layered Architecture */
.arch-diagram.layered {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 20px;
background: #f8f9fa;
border-radius: 6px;
margin-bottom: 20px;
}
.layer-box {
width: 100%;
max-width: 400px;
padding: 16px;
border-radius: 6px;
text-align: center;
border-left: 4px solid;
}
.layer-box.controller {
background: #f0f9ff;
border-left-color: #52c41a;
}
.layer-box.service {
background: #fff7e6;
border-left-color: #fa8c16;
}
.layer-box.repository {
background: #e6f7ff;
border-left-color: #1890ff;
}
.layer-box.domain {
background: #f6ffed;
border-left-color: #73d13d;
}
.layer-title {
font-weight: 600;
color: #1a1a2e;
margin-bottom: 4px;
}
.layer-desc {
font-size: 12px;
color: #666;
}
.arrow {
color: #909399;
font-size: 12px;
text-align: center;
}
.arch-characteristics {
padding: 16px;
background: #f8f9fa;
border-radius: 6px;
}
.arch-characteristics h5 {
margin: 0 0 12px 0;
color: #1a1a2e;
font-size: 14px;
}
.arch-characteristics ul {
margin: 0;
padding-left: 20px;
}
.arch-characteristics li {
margin: 8px 0;
color: #595959;
font-size: 13px;
line-height: 1.5;
}
/* Clean Architecture */
.arch-diagram.clean {
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px;
background: #f8f9fa;
border-radius: 6px;
margin-bottom: 20px;
}
.clean-layers {
display: flex;
flex-direction: column-reverse;
gap: 8px;
}
.clean-layer {
padding: 12px 16px;
border-radius: 6px;
border-left: 4px solid;
}
.clean-layer.framework {
background: #f0f0f0;
border-left-color: #8c8c8c;
}
.clean-layer.interface {
background: #e6f7ff;
border-left-color: #1890ff;
}
.clean-layer.application {
background: #fff7e6;
border-left-color: #fa8c16;
}
.clean-layer.domain {
background: #f6ffed;
border-left-color: #52c41a;
}
.clean-layer .layer-name {
font-weight: 600;
color: #1a1a2e;
font-size: 14px;
margin-bottom: 4px;
}
.clean-layer .layer-items {
font-size: 12px;
color: #666;
}
.dependency-rule {
background: white;
border-radius: 6px;
padding: 16px;
text-align: center;
border: 2px dashed #1890ff;
}
.rule-arrow {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 8px;
}
.rule-arrow .arrow-line {
width: 2px;
height: 20px;
background: #1890ff;
}
.rule-arrow .arrow-head {
color: #1890ff;
font-weight: 600;
font-size: 14px;
}
.rule-text {
color: #595959;
font-size: 13px;
}
/* Comparison Table */
.comparison-table {
overflow-x: auto;
}
.comparison-table table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.comparison-table th,
.comparison-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #e8e8e8;
}
.comparison-table th {
background: #f5f7fa;
font-weight: 600;
color: #1a1a2e;
}
.comparison-table tr:hover {
background: #fafafa;
}
/* Recommendation */
.recommendation {
margin-top: 24px;
}
.recommendation h5 {
margin: 0 0 16px 0;
color: #1a1a2e;
font-size: 15px;
text-align: center;
}
.rec-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.rec-card {
background: #f8f9fa;
border-radius: 6px;
padding: 16px;
position: relative;
}
.rec-card.recommended {
background: #f6ffed;
border: 2px solid #52c41a;
}
.rec-badge {
position: absolute;
top: -10px;
right: 16px;
background: #52c41a;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
}
.rec-title {
font-weight: 600;
color: #1a1a2e;
margin-bottom: 12px;
font-size: 14px;
}
.rec-card ul {
margin: 0;
padding-left: 18px;
}
.rec-card li {
margin: 6px 0;
color: #595959;
font-size: 12px;
line-height: 1.5;
}
.rec-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.rec-card { padding: 14px; border-radius: 6px; background: var(--vp-c-bg-soft); font-size: 12px; }
.rec-card strong { font-size: 13px; color: var(--vp-c-text-1); display: block; margin-bottom: 8px; }
.rec-card ul { margin: 0; padding-left: 16px; }
.rec-card li { margin: 4px 0; color: var(--vp-c-text-2); }
.rec-card.recommended { border: 2px solid var(--vp-c-green-1); background: var(--vp-c-green-soft); }
@media (max-width: 768px) {
.rec-grid {
grid-template-columns: 1fr;
}
.rec-grid { grid-template-columns: 1fr; }
}
</style>
@@ -1,192 +1,70 @@
<template>
<div class="controller-layer-demo">
<div class="demo-header">
<h4>🎮 Controller 请求的"接待员"</h4>
<p class="subtitle">
点击流程节点查看 Controller 如何接收和处理请求
</p>
<div class="controller-demo">
<div class="header">
<div class="title">Controller 请求的"接待员"</div>
<div class="subtitle">点击流程节点查看详情</div>
</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 class="flow">
<div class="step">
<div class="step-label">客户端发起请求</div>
<pre class="step-code">POST /api/users/register
Content-Type: application/json
{ "username": "张三", "email": "zhangsan@example.com", "password": "123456" }</pre>
</div>
<div class="arrow"> 请求到达</div>
<div :class="['step', 'clickable', { active: detail === 'ctrl' }]" @click="toggle('ctrl')">
<div class="step-label accent">Controller 接收并解析请求</div>
<pre class="step-code">@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping("/register")
public ResponseEntity&lt;UserDTO&gt; register(
@RequestBody @Valid UserRegisterRequest request) {
UserDTO user = userService.register(request);
return ResponseEntity.ok(user);
}
}</pre>
</div>
<div class="arrow"> 参数校验 + 调用</div>
<div :class="['step', 'clickable', { active: detail === 'valid' }]" @click="toggle('valid')">
<div class="step-label warn">参数校验Controller 的职责之一</div>
<pre class="step-code">public class UserRegisterRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20) private String username;
@Email(message = "邮箱格式不正确") private String email;
@Size(min = 6, message = "密码至少6位") private String password;
}</pre>
<div v-if="detail === 'valid'" class="detail-box">
<strong>为什么校验要放在 Controller</strong>
<ul>
<li>第一道防线尽早拦截非法请求</li>
<li>减轻下游压力Service 层可以假设数据已清洗</li>
<li>关注点分离Service 专注于业务不处理格式验证</li>
</ul>
</div>
</div>
<div class="arrow-connector">
请求到达
</div>
<div class="arrow"> 返回结果</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&lt;UserDTO&gt; 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 class="step">
<div class="step-label">Controller 封装响应返回</div>
<pre class="step-code">HTTP/1.1 200 OK
{ "code": 200, "message": "注册成功",
"data": { "id": 10001, "username": "张三", "email": "zhangsan@example.com" } }</pre>
</div>
</div>
<!-- Controller 职责总结 -->
<div class="controller-summary">
<h5>🎯 Controller 的核心职责</h5>
<div class="duties">
<div class="duties-title">Controller 的核心职责</div>
<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 class="duty" v-for="d in duties" :key="d.name">
<div class="duty-name">{{ d.name }}</div>
<div class="duty-desc">{{ d.desc }}</div>
</div>
</div>
</div>
@@ -196,196 +74,60 @@
<script setup>
import { ref } from 'vue'
const showDetails = ref('')
const detail = ref('')
const toggle = (s) => { detail.value = detail.value === s ? '' : s }
const toggleDetails = (section) => {
showDetails.value = showDetails.value === section ? '' : section
}
const duties = [
{ name: '接收请求', desc: '映射 HTTP 请求到方法' },
{ name: '参数校验', desc: '基础格式和必填校验' },
{ name: '调用 Service', desc: '将请求转发给业务层' },
{ name: '封装响应', desc: '统一响应格式返回' }
]
</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;
}
.controller-demo { padding: 20px; background: var(--vp-c-bg-soft); border-radius: 12px; }
.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; }
.demo-header {
text-align: center;
margin-bottom: 24px;
}
.flow { display: flex; flex-direction: column; gap: 8px; }
.arrow { text-align: center; color: var(--vp-c-text-3); font-size: 12px; }
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
.step {
padding: 14px; border-radius: 8px;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
}
.step.clickable { cursor: pointer; transition: all .2s; }
.step.clickable:hover { box-shadow: 0 2px 8px rgba(0,0,0,.06); }
.step.active { border-color: var(--vp-c-brand-1); box-shadow: 0 0 0 2px var(--vp-c-brand-soft); }
.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-label { font-weight: 600; font-size: 13px; color: var(--vp-c-text-1); margin-bottom: 8px; }
.step-label.accent { color: #10b981; }
.step-label.warn { color: #f59e0b; }
.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;
margin: 0; padding: 10px; border-radius: 6px; overflow-x: auto;
background: var(--vp-c-bg-soft); font-size: 11px; line-height: 1.5;
color: var(--vp-c-text-2); font-family: var(--vp-font-family-mono);
}
.arrow-connector {
text-align: center;
padding: 8px;
font-size: 12px;
color: #909399;
font-weight: 500;
.detail-box {
margin-top: 12px; padding: 12px; border-radius: 6px;
background: var(--vp-c-brand-soft); border-left: 3px solid var(--vp-c-brand-1);
font-size: 12px; color: var(--vp-c-text-1); line-height: 1.6;
}
.detail-box ul { margin: 8px 0 0; padding-left: 18px; }
.detail-box li { margin: 4px 0; }
.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;
}
.duties { margin-top: 20px; padding: 16px; border-radius: 8px; background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); }
.duties-title { text-align: center; font-weight: 600; font-size: 14px; color: var(--vp-c-text-1); margin-bottom: 12px; }
.duty-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
.duty { text-align: center; padding: 12px 8px; background: var(--vp-c-bg-soft); border-radius: 6px; }
.duty-name { font-weight: 600; font-size: 13px; color: var(--vp-c-text-1); margin-bottom: 4px; }
.duty-desc { font-size: 11px; color: var(--vp-c-text-3); }
@media (max-width: 768px) {
.duty-grid {
grid-template-columns: repeat(2, 1fr);
}
.flow-step {
flex-direction: column;
gap: 8px;
}
.step-content {
width: 100%;
}
.duty-grid { grid-template-columns: repeat(2, 1fr); }
}
</style>
@@ -1,341 +1,83 @@
<template>
<div class="dependency-direction-demo">
<div class="demo-header">
<h4>🔄 依赖方向分层架构的核心规则</h4>
<p class="subtitle">
理解依赖方向才能真正掌握分层架构
</p>
<div class="dep-demo">
<div class="header">
<div class="title">依赖方向分层架构的核心规则</div>
<div class="subtitle">理解依赖方向才能真正掌握分层架构</div>
</div>
<!-- 依赖方向可视化 -->
<div class="direction-visualization">
<div class="arch-diagram">
<!-- 外层 -->
<div class="content-box">
<div class="layers">
<div class="layer outer">
<div class="layer-label">
外层UI / 外部系统
</div>
<div class="layer-box">
Controller
</div>
<div class="layer-label">外层UI / 外部系统</div>
<div class="layer-box">Controller</div>
</div>
<!-- 依赖箭头 -->
<div class="dependency-arrow down">
<span class="arrow-line" />
<span class="arrow-head"> 依赖</span>
</div>
<!-- 中层 -->
<div class="dep-arrow"> 依赖</div>
<div class="layer middle">
<div class="layer-label">
中层应用层
</div>
<div class="layer-box">
Service
</div>
<div class="layer-label">中层应用层</div>
<div class="layer-box">Service</div>
</div>
<!-- 依赖箭头 -->
<div class="dependency-arrow down">
<span class="arrow-line" />
<span class="arrow-head"> 依赖</span>
</div>
<!-- 内层 -->
<div class="dep-arrow"> 依赖</div>
<div class="layer inner">
<div class="layer-label">
内层领域层
</div>
<div class="layer-box">
Domain / Repository
</div>
<div class="layer-label">内层领域层</div>
<div class="layer-box">Domain / Repository</div>
</div>
</div>
<!-- 核心原则说明 -->
<div class="principle-box">
<div class="principle-title">
🎯 核心原则依赖倒置DIP
</div>
<div class="principle-content">
<p><strong>上层模块不应该依赖下层模块的具体实现而应该依赖于抽象</strong></p>
<div class="rule-list">
<div class="rule-item">
<span class="rule-icon"></span>
<div class="rule-text">
<strong>Controller Service 接口</strong>
<div class="rule-desc">
Controller 只依赖 Service 的接口不依赖实现类
</div>
</div>
</div>
<div class="rule-item">
<span class="rule-icon"></span>
<div class="rule-text">
<strong>Service Repository 接口</strong>
<div class="rule-desc">
Service 只依赖 Repository 接口不关心数据怎么存
</div>
</div>
</div>
<div class="rule-item">
<span class="rule-icon"></span>
<div class="rule-text">
<strong>所有层依赖 Domain</strong>
<div class="rule-desc">
Domain 是核心被所有上层依赖 Domain 不依赖任何层
</div>
</div>
</div>
<div class="p-title">核心原则依赖倒置DIP</div>
<p>上层模块不应该依赖下层模块的具体实现而应该依赖于抽象</p>
<div class="rules">
<div v-for="r in rules" :key="r.title" class="rule">
<strong>{{ r.title }}</strong>
<div class="rule-desc">{{ r.desc }}</div>
</div>
</div>
</div>
</div>
<!-- 依赖方向示意图 -->
<div class="direction-diagram">
<h5>📊 依赖方向示意图</h5>
<div class="diagram-content">
<pre class="diagram-code">
Controller Layer
UserController
- @Autowired private IUserService userService;
依赖接口不依赖实现
依赖Dependency
Service Layer
UserServiceImpl
- @Autowired private UserRepository repository;
依赖 Repository 接口
依赖
Repository Layer
UserRepository
- extends JpaRepository&lt;User, Long&gt;
依赖
Domain Layer (核心领域)
User (Entity)
- 不包含任何层依赖
- 被所有层依赖
</pre>
</div>
</div>
</div>
</template>
<script setup>
// Component logic can be added here if needed
const rules = [
{ title: 'Controller → Service 接口', desc: 'Controller 只依赖 Service 的接口,不依赖实现类' },
{ title: 'Service → Repository 接口', desc: 'Service 只依赖 Repository 接口,不关心数据怎么存' },
{ title: '所有层依赖 Domain', desc: 'Domain 是核心,被所有上层依赖,但 Domain 不依赖任何层' }
]
</script>
<style scoped>
.dependency-direction-demo {
padding: 24px;
background: linear-gradient(135deg, #f0f7ff 0%, #e6f0ff 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.dep-demo { padding: 20px; background: var(--vp-c-bg-soft); border-radius: 12px; }
.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; }
.demo-header {
text-align: center;
margin-bottom: 24px;
.content-box {
padding: 18px; border-radius: 10px;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
}
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
.direction-visualization {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.arch-diagram {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.layer {
display: flex;
flex-direction: column;
gap: 8px;
}
.layer-label {
font-size: 11px;
color: #909399;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.layers { display: flex; flex-direction: column; gap: 8px; margin-bottom: 20px; }
.layer-label { font-size: 11px; color: var(--vp-c-text-3); margin-bottom: 4px; }
.layer-box {
padding: 16px 20px;
background: #f5f7fa;
border-radius: 6px;
font-weight: 500;
color: #303133;
text-align: center;
border-left: 4px solid #409eff;
}
.layer.outer .layer-box {
border-left-color: #67c23a;
}
.layer.middle .layer-box {
border-left-color: #e6a23c;
}
.layer.inner .layer-box {
border-left-color: #409eff;
}
.dependency-arrow {
display: flex;
flex-direction: column;
align-items: center;
padding: 4px 0;
}
.arrow-line {
width: 2px;
height: 12px;
background: #dcdfe6;
}
.arrow-head {
color: #909399;
font-size: 11px;
margin-top: 2px;
padding: 14px; border-radius: 6px; text-align: center;
font-weight: 500; color: var(--vp-c-text-1);
background: var(--vp-c-bg-soft); border-left: 3px solid var(--vp-c-divider);
}
.layer.outer .layer-box { border-left-color: #10b981; }
.layer.middle .layer-box { border-left-color: #f59e0b; }
.layer.inner .layer-box { border-left-color: #3b82f6; }
.dep-arrow { text-align: center; color: var(--vp-c-text-3); font-size: 12px; }
.principle-box {
background: linear-gradient(135deg, #e6f7ff 0%, #f0f7ff 100%);
border-radius: 10px;
padding: 20px;
border-left: 4px solid #1890ff;
padding: 16px; border-radius: 8px;
background: var(--vp-c-brand-soft); border-left: 3px solid var(--vp-c-brand-1);
}
.p-title { font-size: 14px; font-weight: 600; color: var(--vp-c-text-1); margin-bottom: 8px; }
.principle-box p { margin: 0 0 12px; font-size: 13px; color: var(--vp-c-text-2); line-height: 1.6; }
.principle-title {
font-size: 15px;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 12px;
}
.principle-content p {
margin: 0 0 12px 0;
color: #595959;
font-size: 13px;
line-height: 1.6;
}
.rule-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.rule-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px;
background: white;
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.rule-icon {
font-size: 16px;
flex-shrink: 0;
}
.rule-text {
flex: 1;
}
.rule-text strong {
color: #1a1a2e;
font-size: 13px;
}
.rule-desc {
color: #8c8c8c;
font-size: 12px;
margin-top: 2px;
}
.direction-diagram {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.direction-diagram h5 {
margin: 0 0 16px 0;
color: #1a1a2e;
font-size: 15px;
text-align: center;
}
.diagram-content {
overflow-x: auto;
}
.diagram-code {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 11px;
line-height: 1.5;
color: #595959;
margin: 0;
white-space: pre;
}
@media (max-width: 768px) {
.model-cards {
grid-template-columns: 1fr;
}
.comparison-tabs {
flex-direction: column;
}
.tab-btn {
width: 100%;
text-align: center;
}
.rules { display: flex; flex-direction: column; gap: 8px; }
.rule {
padding: 10px; border-radius: 6px;
background: var(--vp-c-bg); font-size: 13px; color: var(--vp-c-text-1);
}
.rule-desc { font-size: 12px; color: var(--vp-c-text-3); margin-top: 2px; }
</style>
@@ -1,298 +1,68 @@
<template>
<div class="domain-model-demo">
<div class="demo-header">
<h4>📦 Domain 领域模型设计</h4>
<p class="subtitle">
Domain 是业务概念的载体所有层的依赖基础
</p>
<div class="domain-demo">
<div class="header">
<div class="title">Domain 领域模型设计</div>
<div class="subtitle">Domain 是业务概念的载体所有层的依赖基础</div>
</div>
<!-- 领域模型对比 -->
<div class="model-comparison">
<div class="comparison-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-btn', { active: currentTab === tab.id }]"
@click="currentTab = tab.id"
>
{{ tab.name }}
</button>
<div class="tabs">
<button
v-for="t in tabs" :key="t.id"
:class="['tab', { active: current === t.id }]"
@click="current = t.id"
>{{ t.name }}</button>
</div>
<div v-if="current === 'comparison'" class="cards">
<div class="card bad">
<div class="card-head">
<span class="card-title">贫血模型 (Anemic)</span>
<span class="card-badge bad">传统做法</span>
</div>
<pre class="code"><code>{{ anemicEntity }}</code></pre>
<pre class="code"><code>{{ anemicService }}</code></pre>
<div class="result-box bad">
<strong>贫血模型的问题</strong>
<ul>
<li>违背面向对象对象只有数据没有行为</li>
<li>逻辑分散同样的规则可能在多个 Service 重复</li>
<li>难以维护改一个规则要找所有用到的地方</li>
</ul>
</div>
</div>
<div class="comparison-content">
<!-- 贫血模型 vs 充血模型 -->
<div
v-if="currentTab === 'comparison'"
class="tab-panel"
>
<div class="model-cards">
<div class="model-card anemic">
<div class="card-header">
<span class="card-icon">📄</span>
<span class="card-title">贫血模型 (Anemic)</span>
<span class="card-badge">传统做法</span>
</div>
<div class="card-content">
<div class="code-section">
<div class="code-label">
Entity只有 getter/setter
</div>
<pre><code>@Entity
public class Order {
@Id
private Long id;
private Long userId;
private BigDecimal totalAmount;
private OrderStatus status;
private LocalDateTime createdAt;
// 只有 getter/setter,没有业务逻辑
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
// ... 其他 getter/setter
}</code></pre>
</div>
<div class="code-section">
<div class="code-label">
Service所有业务逻辑都在这里
</div>
<pre><code>@Service
public class OrderService {
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow();
// 贫血模型:业务逻辑散落在 Service 里
if (order.getStatus() == OrderStatus.SHIPPED) {
throw new IllegalStateException("已发货订单不能取消");
}
if (order.getStatus() == OrderStatus.CANCELLED) {
throw new IllegalStateException("订单已取消");
}
// 修改状态
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}
}</code></pre>
</div>
<div class="problems">
<div class="problem-title">
😫 贫血模型的问题
</div>
<ul>
<li><strong>违背面向对象</strong>对象只有数据没有行为变成了 "数据结构"</li>
<li><strong>逻辑分散</strong>同样的业务规则可能在多个 Service 重复</li>
<li><strong>难以维护</strong>改一个规则要找所有用到的地方</li>
</ul>
</div>
</div>
</div>
<div class="model-card rich">
<div class="card-header">
<span class="card-icon">🧠</span>
<span class="card-title">充血模型 (Rich Domain)</span>
<span class="card-badge tag-green">推荐做法</span>
</div>
<div class="card-content">
<div class="code-section">
<div class="code-label">
Entity包含业务逻辑
</div>
<pre><code>@Entity
public class Order {
@Id
private Long id;
private Long userId;
private BigDecimal totalAmount;
private OrderStatus status;
private LocalDateTime createdAt;
// 🎯 业务行为封装在实体里
/**
* 取消订单
*/
public void cancel() {
// 状态校验内聚在方法里
if (this.status == OrderStatus.SHIPPED) {
throw new IllegalStateException("已发货订单不能取消");
}
if (this.status == OrderStatus.CANCELLED) {
throw new IllegalStateException("订单已取消");
}
this.status = OrderStatus.CANCELLED;
// 可以触发领域事件
registerEvent(new OrderCancelledEvent(this.id));
}
/**
* 支付订单
*/
public void pay(Payment payment) {
if (this.status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("订单状态不正确");
}
if (!payment.getAmount().equals(this.totalAmount)) {
throw new IllegalArgumentException("支付金额不匹配");
}
this.status = OrderStatus.PAID;
this.paymentTime = LocalDateTime.now();
}
// ... 其他业务方法
}</code></pre>
</div>
<div class="code-section">
<div class="code-label">
Service只做协调不做业务判断
</div>
<pre><code>@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final DomainEventPublisher eventPublisher;
@Transactional
public void cancelOrder(Long orderId) {
// 1. 加载聚合根
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
// 2. 💡 调用领域对象的业务方法
// 业务规则封装在 Order 里,Service 只做协调
order.cancel();
// 3. 保存变更
orderRepository.save(order);
// 4. 发布领域事件
order.getDomainEvents().forEach(eventPublisher::publish);
order.clearDomainEvents();
}
}</code></pre>
</div>
<div class="benefits">
<div class="benefit-title">
😊 充血模型的优势
</div>
<ul>
<li><strong>符合面向对象</strong>数据和行为封装在一起是真正的 "对象"</li>
<li><strong>业务内聚</strong>规则跟着对象走改一处处处生效</li>
<li><strong>可复用可测试</strong>领域对象是纯内存对象单元测试不需要数据库</li>
<li><strong>表达力强</strong>order.cancel() orderService.cancel(order) 更自然</li>
</ul>
</div>
</div>
</div>
</div>
<div class="card good">
<div class="card-head">
<span class="card-title">充血模型 (Rich Domain)</span>
<span class="card-badge good">推荐做法</span>
</div>
<pre class="code"><code>{{ richEntity }}</code></pre>
<pre class="code"><code>{{ richService }}</code></pre>
<div class="result-box good">
<strong>充血模型的优势</strong>
<ul>
<li>符合面向对象数据和行为封装在一起</li>
<li>业务内聚规则跟着对象走改一处处处生效</li>
<li>可测试领域对象是纯内存对象不需要数据库</li>
<li>表达力强order.cancel() orderService.cancel(order) 更自然</li>
</ul>
</div>
</div>
</div>
<!-- 值对象 -->
<div
v-else-if="currentTab === 'valueobject'"
class="tab-panel"
>
<div class="value-object-content">
<div class="concept-intro">
<h5>💎 什么是值对象Value Object</h5>
<p>值对象是没有唯一标识不可变的对象它描述了某种特征或属性两个值对象如果所有属性相等就被认为是同一个对象</p>
</div>
<div class="vo-examples">
<div class="example-card">
<div class="example-title">
📍 地址 Address
</div>
<pre><code>// 值对象:不可变、无 ID
public record Address(
String province, // 省
String city, // 市
String district, // 区
String street, // 街道
String zipCode // 邮编
) {
// 值对象的方法通常是转换或计算
public String toDisplayString() {
return String.format("%s%s%s%s",
province, city, district, street);
}
// 校验逻辑
public boolean isValid() {
return StringUtils.isNotBlank(province)
&& StringUtils.isNotBlank(city);
}
}
// 使用:地址相等只要属性相同
Address addr1 = new Address("广东", "深圳", "南山", "科技园", "518000");
Address addr2 = new Address("广东", "深圳", "南山", "科技园", "518000");
System.out.println(addr1.equals(addr2)); // true - 值对象比较的是值</code></pre>
</div>
<div class="example-card">
<div class="example-title">
💰 金钱 Money
</div>
<pre><code>// 金钱是经典的值对象
public record Money(
BigDecimal amount,
Currency currency
) {
// 工厂方法
public static Money of(BigDecimal amount, String currencyCode) {
return new Money(amount, Currency.getInstance(currencyCode));
}
public static Money yuan(BigDecimal amount) {
return new Money(amount, Currency.getInstance("CNY"));
}
// 值对象的核心:运算返回新的值对象
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add different currencies");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int factor) {
return new Money(this.amount.multiply(BigDecimal.valueOf(factor)), currency);
}
public boolean isGreaterThan(Money other) {
return this.amount.compareTo(other.amount) > 0;
}
// 格式化显示
public String toDisplayString() {
return currency.getSymbol() + amount.setScale(2, RoundingMode.HALF_UP);
}
}
// 使用示例
Money price = Money.yuan(new BigDecimal("199.99"));
Money shipping = Money.yuan(new BigDecimal("10.00"));
Money discount = Money.yuan(new BigDecimal("20.00"));
Money total = price.add(shipping).add(discount.negate());
System.out.println(total.toDisplayString()); // ¥189.99</code></pre>
</div>
</div>
</div>
<div v-else class="vo-section">
<div class="vo-intro">
<strong>什么是值对象Value Object</strong>
<p>没有唯一标识不可变的对象描述某种特征或属性两个值对象所有属性相等就被认为是同一个</p>
</div>
<div class="vo-examples">
<div class="vo-card">
<div class="vo-name">地址 Address</div>
<pre class="code"><code>{{ addressVO }}</code></pre>
</div>
<div class="vo-card">
<div class="vo-name">金钱 Money</div>
<pre class="code"><code>{{ moneyVO }}</code></pre>
</div>
</div>
</div>
@@ -302,224 +72,142 @@ System.out.println(total.toDisplayString()); // ¥189.99</code></pre>
<script setup>
import { ref } from 'vue'
const currentTab = ref('comparison')
const current = ref('comparison')
const tabs = [
{ id: 'comparison', name: '贫血 vs 充血' },
{ id: 'valueobject', name: '值对象设计' }
]
const anemicEntity = `@Entity
public class Order {
@Id private Long id;
private BigDecimal totalAmount;
private OrderStatus status;
// 只有 getter/setter,没有业务逻辑
public Long getId() { return id; }
public void setStatus(OrderStatus s) { this.status = s; }
}`
const anemicService = `@Service
public class OrderService {
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
// 贫血模型:业务逻辑散落在 Service 里
if (order.getStatus() == OrderStatus.SHIPPED)
throw new IllegalStateException("已发货不能取消");
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}
}`
const richEntity = `@Entity
public class Order {
@Id private Long id;
private BigDecimal totalAmount;
private OrderStatus status;
// 业务行为封装在实体里
public void cancel() {
if (this.status == OrderStatus.SHIPPED)
throw new IllegalStateException("已发货不能取消");
this.status = OrderStatus.CANCELLED;
registerEvent(new OrderCancelledEvent(this.id));
}
public void pay(Payment payment) {
if (this.status != OrderStatus.PENDING_PAYMENT)
throw new IllegalStateException("状态不正确");
this.status = OrderStatus.PAID;
}
}`
const richService = `@Service
public class OrderService {
@Transactional
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
order.cancel(); // 调用领域对象的业务方法
orderRepository.save(order);
}
}`
const addressVO = `// 值对象:不可变、无 ID
public record Address(String province, String city, String district, String street) {
public String toDisplayString() {
return String.format("%s%s%s%s", province, city, district, street);
}
}
// 地址相等只要属性相同
Address a1 = new Address("广东", "深圳", "南山", "科技园");
Address a2 = new Address("广东", "深圳", "南山", "科技园");
a1.equals(a2); // true`
const moneyVO = `public record Money(BigDecimal amount, Currency currency) {
public static Money yuan(BigDecimal amount) {
return new Money(amount, Currency.getInstance("CNY"));
}
// 运算返回新的值对象(不可变)
public Money add(Money other) {
if (!this.currency.equals(other.currency))
throw new IllegalArgumentException("Cannot add different currencies");
return new Money(this.amount.add(other.amount), this.currency);
}
}
Money price = Money.yuan(new BigDecimal("199.99"));
Money shipping = Money.yuan(new BigDecimal("10.00"));
Money total = price.add(shipping); // ¥209.99`
</script>
<style scoped>
.domain-model-demo {
padding: 24px;
background: linear-gradient(135deg, #f0f7ff 0%, #e6f0ff 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.domain-demo { padding: 20px; background: var(--vp-c-bg-soft); border-radius: 12px; }
.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; }
.demo-header {
text-align: center;
margin-bottom: 24px;
.tabs { display: flex; gap: 8px; margin-bottom: 16px; }
.tab {
padding: 7px 16px; border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg);
border-radius: 6px; cursor: pointer; font-size: 13px; color: var(--vp-c-text-2); transition: all .2s;
}
.tab:hover { color: var(--vp-c-brand-1); border-color: var(--vp-c-brand-1); }
.tab.active { background: var(--vp-c-brand-1); border-color: var(--vp-c-brand-1); color: #fff; }
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
.cards { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.card {
padding: 16px; border-radius: 10px;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
}
.card.bad { border-left: 3px solid var(--vp-c-danger-1); }
.card.good { border-left: 3px solid var(--vp-c-green-1); }
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.card-title { font-weight: 600; font-size: 14px; color: var(--vp-c-text-1); }
.card-badge { padding: 2px 8px; border-radius: 10px; font-size: 11px; color: #fff; }
.card-badge.bad { background: var(--vp-c-danger-1); }
.card-badge.good { background: var(--vp-c-green-1); }
.model-comparison {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
.code {
margin: 0 0 12px; padding: 10px; border-radius: 6px; overflow-x: auto;
background: var(--vp-code-block-bg); font-size: 10px; line-height: 1.5;
}
.code code { color: var(--vp-c-text-1); font-family: var(--vp-font-family-mono); }
.comparison-tabs {
display: flex;
background: #f5f7fa;
border-bottom: 1px solid #e4e7ed;
}
.result-box { padding: 10px; border-radius: 6px; font-size: 12px; line-height: 1.5; }
.result-box.bad { background: var(--vp-c-danger-soft); border-left: 3px solid var(--vp-c-danger-1); }
.result-box.good { background: var(--vp-c-green-soft); border-left: 3px solid var(--vp-c-green-1); }
.result-box strong { font-size: 12px; color: var(--vp-c-text-1); }
.result-box ul { margin: 6px 0 0; padding-left: 16px; }
.result-box li { margin: 3px 0; color: var(--vp-c-text-2); }
.tab-btn {
padding: 12px 24px;
border: none;
background: transparent;
color: #606266;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.tab-btn:hover {
color: #409eff;
}
.tab-btn.active {
color: #409eff;
background: white;
font-weight: 500;
}
.tab-btn.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: #409eff;
}
.comparison-content {
padding: 20px;
}
.tab-panel {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.model-cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.model-card {
background: #f8f9fa;
border-radius: 10px;
overflow: hidden;
border: 1px solid #e4e7ed;
}
.model-card.anemic {
border-left: 4px solid #ff4d4f;
}
.model-card.rich {
border-left: 4px solid #52c41a;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: white;
border-bottom: 1px solid #e4e7ed;
}
.card-icon {
font-size: 20px;
}
.card-title {
font-weight: 600;
color: #303133;
font-size: 14px;
}
.card-badge {
margin-left: auto;
padding: 2px 8px;
background: #ff4d4f;
color: white;
border-radius: 10px;
font-size: 11px;
}
.card-badge.tag-green {
background: #52c41a;
}
.card-content {
padding: 16px;
}
.code-section {
margin-bottom: 16px;
}
.code-label {
font-size: 11px;
color: #909399;
margin-bottom: 6px;
font-weight: 500;
}
.code-section pre {
background: #2d2d2d;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin: 0;
}
.code-section code {
color: #f8f8f2;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 10px;
line-height: 1.5;
}
.problems, .benefits {
padding: 12px;
border-radius: 6px;
margin-top: 12px;
}
.problems {
background: #fff1f0;
border-left: 3px solid #ff4d4f;
}
.benefits {
background: #f6ffed;
border-left: 3px solid #52c41a;
}
.problem-title, .benefit-title {
font-weight: 600;
font-size: 13px;
margin-bottom: 8px;
}
.problem-title {
color: #cf1322;
}
.benefit-title {
color: #389e0d;
}
.problems ul, .benefits ul {
margin: 0;
padding-left: 16px;
}
.problems li, .benefits li {
margin: 6px 0;
font-size: 12px;
line-height: 1.5;
color: #595959;
}
.vo-section { background: var(--vp-c-bg); border-radius: 10px; padding: 18px; border: 1px solid var(--vp-c-divider); }
.vo-intro { margin-bottom: 16px; font-size: 13px; color: var(--vp-c-text-2); line-height: 1.6; }
.vo-intro strong { color: var(--vp-c-text-1); }
.vo-intro p { margin: 6px 0 0; }
.vo-examples { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.vo-card { background: var(--vp-c-bg-soft); border-radius: 8px; padding: 14px; }
.vo-name { font-weight: 600; font-size: 13px; color: var(--vp-c-text-1); margin-bottom: 8px; }
@media (max-width: 1024px) {
.model-cards {
grid-template-columns: 1fr;
}
.cards, .vo-examples { grid-template-columns: 1fr; }
}
</style>
@@ -1,362 +1,113 @@
<template>
<div class="dto-flow-demo">
<div class="demo-header">
<h4>🔄 DTO 流转数据在不同层之间的转换</h4>
<p class="subtitle">
DTOData Transfer Object是层与层之间传递数据的载体
</p>
<div class="dto-demo">
<div class="header">
<div class="title">DTO 流转数据在不同层之间的转换</div>
<div class="subtitle">DTOData Transfer Object是层与层之间传递数据的载体</div>
</div>
<!-- 流程图 -->
<div class="flow-diagram">
<div class="flow-step">
<div class="step-title">
Controller
</div>
<div class="step-code">
<div class="code-line">
<span class="comment">// 接收 Request DTO</span>
</div>
<div class="code-line">
<span class="keyword">public</span> ResponseEntity&lt;UserDTO&gt; createUser(
</div>
<div class="code-line">
&nbsp;&nbsp;@RequestBody <span class="highlight">@Valid UserCreateRequest request</span>
</div>
<div class="code-line">
) { ... }
</div>
</div>
<div class="flow-box">
<div class="flow-step green">
<div class="step-label">Controller </div>
<pre class="step-code"><code>// 接收 Request DTO
public ResponseEntity&lt;UserDTO&gt; createUser(
@RequestBody @Valid UserCreateRequest request) { ... }</code></pre>
</div>
<div class="flow-arrow">
转换为 Service 需要的参数
<div class="arrow"> 转换为 Service 需要的参数</div>
<div class="flow-step orange">
<div class="step-label">Service </div>
<pre class="step-code"><code>public UserDTO createUser(UserCreateParam param) {
User user = param.toEntity(); // 转换为 Entity
userRepository.save(user);
return UserDTO.from(user); // Entity → DTO
}</code></pre>
</div>
<div class="arrow"> 转换为 Repository 需要的 Entity</div>
<div class="flow-step blue">
<div class="step-label">Repository </div>
<pre class="step-code"><code>public interface UserRepository
extends JpaRepository&lt;User, Long&gt; { }</code></pre>
</div>
<div class="arrow"> 返回 Entity转换为 DTO</div>
<div class="flow-step">
<div class="step-title">
Service
</div>
<div class="step-code">
<div class="code-line">
<span class="comment">// 业务处理</span>
</div>
<div class="code-line">
<span class="keyword">public</span> UserDTO createUser(UserCreateParam param) {
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="comment">// 转换为 Entity</span>
</div>
<div class="code-line">
&nbsp;&nbsp;User user = <span class="highlight">param.toEntity()</span>;
</div>
<div class="code-line">
&nbsp;&nbsp;userRepository.save(user);
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="keyword">return</span> <span class="highlight">UserDTO.from(user)</span>;
</div>
<div class="code-line">
}
</div>
</div>
</div>
<div class="flow-arrow">
转换为 Repository 需要的 Entity
</div>
<div class="flow-step">
<div class="step-title">
Repository
</div>
<div class="step-code">
<div class="code-line">
<span class="comment">// 数据持久化</span>
</div>
<div class="code-line">
<span class="keyword">public interface</span> UserRepository
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="keyword">extends</span> JpaRepository&lt;<span class="highlight">User</span>, Long&gt; {
</div>
<div class="code-line">
}
</div>
</div>
</div>
<div class="flow-arrow">
返回 Entity转换为 DTO
</div>
<div class="flow-step">
<div class="step-title">
返回给客户端
</div>
<div class="step-code">
<div class="code-line">
<span class="comment">// Response DTO</span>
</div>
<div class="code-line">
{
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="string">"id"</span>: 10001,
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="string">"username"</span>: <span class="string">"张三"</span>,
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="string">"email"</span>: <span class="string">"zhangsan@example.com"</span>,
</div>
<div class="code-line">
&nbsp;&nbsp;<span class="string">"createdAt"</span>: <span class="string">"2024-01-15T10:30:00Z"</span>
</div>
<div class="code-line">
}
</div>
</div>
<div class="step-label">返回给客户端</div>
<pre class="step-code"><code>{ "id": 10001, "username": "张三",
"email": "zhangsan@example.com", "createdAt": "2024-01-15T10:30:00Z" }</code></pre>
</div>
</div>
<!-- 不同层 DTO 对比 -->
<div class="dto-comparison">
<h5>📋 不同层的 DTO 职责</h5>
<div class="comparison-table">
<div class="table-header">
<div class="col-layer">
层级
</div>
<div class="col-dto">
DTO 类型
</div>
<div class="col-purpose">
职责
</div>
<div class="col-example">
示例
</div>
</div>
<div class="table-row">
<div class="col-layer">
<span class="layer-tag controller">Controller</span>
</div>
<div class="col-dto">
Request / Response DTO
</div>
<div class="col-purpose">
定义 API 契约参数校验序列化
</div>
<div class="col-example">
<code>UserCreateRequest</code>
</div>
</div>
<div class="table-row">
<div class="col-layer">
<span class="layer-tag service">Service</span>
</div>
<div class="col-dto">
Param / Result DTO
</div>
<div class="col-purpose">
封装业务方法参数解耦 Controller Service
</div>
<div class="col-example">
<code>UserCreateParam</code>
</div>
</div>
<div class="table-row">
<div class="col-layer">
<span class="layer-tag repository">Repository</span>
</div>
<div class="col-dto">
Entity / DO
</div>
<div class="col-purpose">
映射数据库表结构ORM 映射
</div>
<div class="col-example">
<code>UserEntity</code>
</div>
</div>
</div>
<div class="table-box">
<div class="table-title">不同层的 DTO 职责</div>
<table>
<thead>
<tr><th>层级</th><th>DTO 类型</th><th>职责</th><th>示例</th></tr>
</thead>
<tbody>
<tr v-for="r in rows" :key="r.layer">
<td><span :class="['tag', r.cls]">{{ r.layer }}</span></td>
<td>{{ r.type }}</td>
<td>{{ r.purpose }}</td>
<td><code>{{ r.example }}</code></td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const viewMode = ref('conversion')
const rows = [
{ layer: 'Controller', cls: 'green', type: 'Request / Response DTO', purpose: '定义 API 契约、参数校验', example: 'UserCreateRequest' },
{ layer: 'Service', cls: 'orange', type: 'Param / Result DTO', purpose: '封装业务方法参数,解耦层间依赖', example: 'UserCreateParam' },
{ layer: 'Repository', cls: 'blue', type: 'Entity / DO', purpose: '映射数据库表结构', example: 'UserEntity' }
]
</script>
<style scoped>
.dto-flow-demo {
padding: 24px;
background: linear-gradient(135deg, #f0f7ff 0%, #e6f0ff 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.dto-demo { padding: 20px; background: var(--vp-c-bg-soft); border-radius: 12px; }
.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; }
.demo-header {
text-align: center;
margin-bottom: 24px;
.flow-box {
padding: 18px; border-radius: 10px; margin-bottom: 16px;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
}
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
}
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
}
.flow-diagram {
background: white;
border-radius: 10px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.flow-step {
background: #f8f9fa;
border-radius: 6px;
overflow: hidden;
border-left: 4px solid #409eff;
border-radius: 6px; overflow: hidden;
background: var(--vp-c-bg-soft); border-left: 3px solid var(--vp-c-divider);
}
.flow-step.green { border-left-color: #10b981; }
.flow-step.orange { border-left-color: #f59e0b; }
.flow-step.blue { border-left-color: #3b82f6; }
.flow-step:nth-child(odd) {
border-left-color: #67c23a;
.step-label {
padding: 10px 14px; font-weight: 600; font-size: 13px;
color: var(--vp-c-text-1); border-bottom: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
}
.step-title {
padding: 12px 16px;
background: white;
font-weight: 600;
color: #303133;
font-size: 14px;
border-bottom: 1px solid #ebeef5;
}
.step-code {
padding: 16px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 12px;
line-height: 1.6;
margin: 0; padding: 12px 14px; overflow-x: auto;
font-size: 11px; line-height: 1.5;
}
.step-code code { color: var(--vp-c-text-2); font-family: var(--vp-font-family-mono); }
.arrow { text-align: center; padding: 8px; color: var(--vp-c-text-3); font-size: 12px; }
.code-line {
padding: 2px 0;
}
.comment {
color: #6a9955;
}
.keyword {
color: #569cd6;
}
.highlight {
background: #fff3cd;
padding: 2px 4px;
border-radius: 3px;
color: #856404;
}
.string {
color: #ce9178;
}
.flow-arrow {
text-align: center;
padding: 12px;
color: #909399;
font-size: 13px;
font-weight: 500;
}
.dto-comparison {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.dto-comparison h5 {
margin: 0 0 16px 0;
color: #1a1a2e;
font-size: 15px;
text-align: center;
}
.comparison-table {
overflow-x: auto;
}
.table-header, .table-row {
display: grid;
grid-template-columns: 100px 150px 1fr 120px;
gap: 12px;
padding: 12px;
align-items: center;
}
.table-header {
background: #f5f7fa;
border-radius: 6px;
font-weight: 600;
color: #303133;
font-size: 13px;
}
.table-row {
border-bottom: 1px solid #ebeef5;
font-size: 12px;
color: #606266;
}
.table-row:last-child {
border-bottom: none;
}
.layer-tag {
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
display: inline-block;
}
.layer-tag.controller {
background: #f0f9ff;
color: #1890ff;
}
.layer-tag.service {
background: #fff7e6;
color: #fa8c16;
}
.layer-tag.repository {
background: #f6ffed;
color: #52c41a;
}
@media (max-width: 768px) {
.table-header, .table-row {
grid-template-columns: 1fr;
gap: 8px;
}
.table-header {
display: none;
}
.table-box {
padding: 16px; border-radius: 10px;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
}
.table-title { text-align: center; font-weight: 600; font-size: 14px; color: var(--vp-c-text-1); margin-bottom: 12px; }
table { width: 100%; border-collapse: collapse; font-size: 12px; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid var(--vp-c-divider); color: var(--vp-c-text-2); }
th { background: var(--vp-c-bg-soft); font-weight: 600; color: var(--vp-c-text-1); }
.tag { padding: 2px 8px; border-radius: 10px; font-size: 11px; color: #fff; }
.tag.green { background: #10b981; }
.tag.orange { background: #f59e0b; }
.tag.blue { background: #3b82f6; }
</style>
@@ -1,398 +1,154 @@
<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 class="layered-arch-demo">
<div class="header">
<div class="title">后端四层架构总览</div>
<div class="subtitle">点击各层查看详细说明</div>
</div>
<div class="main">
<div class="layers">
<div class="client-box">客户端 (Web / App)</div>
<div class="arrow"> HTTP</div>
<div
v-for="layer in layers"
:key="layer.id"
:class="['layer-box', layer.id, { active: active === layer.id }]"
@click="active = active === layer.id ? '' : layer.id"
>
<div class="layer-header">
<span class="layer-name">{{ layer.name }}</span>
<span class="layer-badge">{{ layer.badge }}</span>
</div>
<div class="layer-duty">{{ layer.duty }}</div>
</div>
<div class="arrow-down">
HTTP/HTTPS
</div>
<div class="arrow"> SQL</div>
<div class="client-box db">数据库 (MySQL / PostgreSQL)</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>
<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>
<ul>
<li
v-for="mistake in layerInfo.mistakes"
:key="mistake"
>
{{ mistake }}
</li>
<li v-for="m in activeInfo.mistakes" :key="m">{{ m }}</li>
</ul>
</div>
</div>
</div>
<!-- 底部交互提示 -->
<div class="interaction-hint">
💡 点击各层查看详细说明 | 实际调用流向从上到下依赖从下到上
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeLayer = ref('')
const active = ref('')
const setActiveLayer = (layer) => {
activeLayer.value = activeLayer.value === layer ? '' : layer
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 对象之间循环依赖']
}
}
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] || {}
})
const activeInfo = computed(() => infoMap[active.value] || {})
</script>
<style scoped>
.layered-architecture-demo {
.layered-arch-demo {
padding: 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
background: var(--vp-c-bg-soft);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.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; }
.architecture-container {
display: flex;
gap: 20px;
align-items: flex-start;
}
.main { display: flex; gap: 20px; align-items: flex-start; }
.layers { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.client-layer {
display: flex;
flex-direction: column;
align-items: center;
}
.backend-layers {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
.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);
}
.client-box.db { border-left: 3px solid #8b5cf6; }
.arrow { text-align: center; color: var(--vp-c-text-3); font-size: 12px; padding: 2px; }
.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;
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); }
.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;
padding: 1px 8px; border-radius: 10px; font-size: 11px;
background: var(--vp-c-bg-soft); color: var(--vp-c-text-3);
}
.layer-duty { font-size: 12px; color: var(--vp-c-text-2); }
.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;
width: 300px; padding: 18px; border-radius: 10px;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
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-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-panel p {
margin: 0 0 16px 0;
color: #606266;
font-size: 13px;
line-height: 1.7;
.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; }
.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;
}
@media (max-width: 768px) {
.main { flex-direction: column; }
.info-panel { width: 100%; position: static; }
}
</style>
@@ -1,123 +1,57 @@
<template>
<div class="service-layer-demo">
<div class="demo-header">
<h4> Service 业务逻辑的"指挥家"</h4>
<p class="subtitle">
Service 层编排业务逻辑协调多个 Repository管理事务边界
</p>
<div class="service-demo">
<div class="header">
<div class="title">Service 业务逻辑的"指挥家"</div>
<div class="subtitle">选择业务场景查看 Service 层如何编排逻辑</div>
</div>
<!-- 场景选择器 -->
<div class="scenario-selector">
<div class="selector-label">
选择业务场景
</div>
<div class="scenario-buttons">
<button
v-for="scenario in scenarios"
:key="scenario.id"
:class="['scenario-btn', { active: currentScenario === scenario.id }]"
@click="currentScenario = scenario.id"
>
{{ scenario.name }}
</button>
</div>
<div class="tabs">
<button
v-for="s in scenarios" :key="s.id"
:class="['tab', { active: current === s.id }]"
@click="current = s.id; expanded = []"
>{{ s.name }}</button>
</div>
<!-- 流程图 -->
<div class="flow-diagram">
<div class="flow-header">
<span class="flow-title">{{ currentScenarioData.title }}</span>
<span class="flow-desc">{{ currentScenarioData.description }}</span>
</div>
<div class="flow-box">
<div class="flow-title">{{ data.title }}</div>
<div class="flow-desc">{{ data.desc }}</div>
<div class="flow-steps">
<div class="steps">
<div
v-for="(step, index) in currentScenarioData.steps"
:key="index"
class="flow-step"
:class="{ 'has-sub-steps': step.subSteps }"
@click="toggleStep(index)"
v-for="(step, i) in data.steps" :key="i"
class="step" @click="toggleStep(i)"
>
<div class="step-header">
<div class="step-number">
{{ index + 1 }}
</div>
<div class="step-head">
<span class="step-num">{{ i + 1 }}</span>
<div class="step-info">
<div class="step-name">
{{ step.name }}
</div>
<div class="step-layer">
{{ step.layer }}
</div>
</div>
<div
v-if="step.subSteps"
class="expand-icon"
>
{{ expandedSteps.includes(index) ? '▼' : '▶' }}
<div class="step-name">{{ step.name }}</div>
<div class="step-layer">{{ step.layer }}</div>
</div>
<span v-if="step.subs" class="expand">{{ expanded.includes(i) ? '' : '' }}</span>
</div>
<div
v-if="step.code"
class="step-code"
>
<pre><code>{{ step.code }}</code></pre>
</div>
<!-- 子步骤事务管理 -->
<div
v-if="step.subSteps && expandedSteps.includes(index)"
class="sub-steps"
>
<div
v-for="(subStep, subIndex) in step.subSteps"
:key="subIndex"
class="sub-step"
:class="subStep.status"
>
<div class="sub-step-icon">
{{ subStep.icon }}
</div>
<div class="sub-step-content">
<div class="sub-step-name">
{{ subStep.name }}
</div>
<div class="sub-step-desc">
{{ subStep.desc }}
</div>
</div>
<div class="sub-step-status">
{{ subStep.statusText }}
<pre v-if="step.code" class="step-code"><code>{{ step.code }}</code></pre>
<div v-if="step.subs && expanded.includes(i)" class="subs">
<div v-for="(sub, j) in step.subs" :key="j" class="sub">
<span class="sub-icon">{{ sub.icon }}</span>
<div class="sub-info">
<div class="sub-name">{{ sub.name }}</div>
<div class="sub-desc">{{ sub.desc }}</div>
</div>
<span class="sub-status">{{ sub.status }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Service 设计原则 -->
<div class="design-principles">
<h5>🎯 Service 层设计原则</h5>
<div class="principles-grid">
<div
v-for="principle in principles"
:key="principle.id"
class="principle-card"
>
<div class="principle-icon">
{{ principle.icon }}
</div>
<div class="principle-title">
{{ principle.title }}
</div>
<div class="principle-desc">
{{ principle.desc }}
</div>
<div class="principle-example">
<code>{{ principle.example }}</code>
</div>
<div class="principles">
<div class="principles-title">Service 层设计原则</div>
<div class="principle-grid">
<div v-for="p in principles" :key="p.title" class="principle">
<div class="p-title">{{ p.title }}</div>
<div class="p-desc">{{ p.desc }}</div>
<code class="p-example">{{ p.example }}</code>
</div>
</div>
</div>
@@ -127,8 +61,8 @@
<script setup>
import { ref, computed } from 'vue'
const currentScenario = ref('order')
const expandedSteps = ref([])
const current = ref('order')
const expanded = ref([])
const scenarios = [
{ id: 'order', name: '下单流程' },
@@ -136,596 +70,201 @@ const scenarios = [
{ id: 'report', name: '报表生成' }
]
const scenarioData = {
const allData = {
order: {
title: '🛒 电商下单流程',
description: '用户下单涉及库存扣减、订单创建、支付记录等多个操作,需保证事务一致性',
title: '电商下单流程',
desc: '用户下单涉及库存扣减、订单创建、支付记录,需保证事务一致性',
steps: [
{
name: '参数校验与DTO转换',
layer: 'Controller',
{ name: '参数校验与DTO转换', layer: 'Controller',
code: `@PostMapping("/orders")
public ResponseEntity&lt;OrderDTO&gt; createOrder(
@RequestBody @Valid CreateOrderRequest request
) {
// 调用 Service
public ResponseEntity<OrderDTO> createOrder(
@RequestBody @Valid CreateOrderRequest request) {
OrderDTO order = orderService.createOrder(request);
return ResponseEntity.ok(order);
}`
},
{
name: '业务逻辑编排(事务管理)',
layer: 'Service',
code: `@Service
@Transactional // 关键:事务管理
public class OrderService {
public OrderDTO createOrder(CreateOrderRequest request) {
// 1. 检查库存
inventoryService.checkAndDeduct(request.getSkuId(),
request.getQuantity());
// 2. 创建订单
Order order = new Order();
order.setUserId(request.getUserId());
order.setTotalAmount(calculateTotal(request));
orderRepository.save(order);
// 3. 创建支付记录
Payment payment = createPayment(order);
paymentRepository.save(payment);
// 任一失败都会回滚
return convertToDTO(order);
}
}` },
{ name: '业务逻辑编排(事务管理)', layer: 'Service',
code: `@Transactional
public OrderDTO createOrder(CreateOrderRequest request) {
inventoryService.checkAndDeduct(request.getSkuId(), request.getQuantity());
Order order = new Order();
order.setUserId(request.getUserId());
order.setTotalAmount(calculateTotal(request));
orderRepository.save(order);
Payment payment = createPayment(order);
paymentRepository.save(payment);
return convertToDTO(order);
}`,
subSteps: [
{
icon: '✅',
name: '检查并扣减库存',
desc: '确保库存充足,预先锁定',
status: 'success',
statusText: '成功'
},
{
icon: '📝',
name: '创建订单记录',
desc: '生成订单主表数据',
status: 'success',
statusText: '成功'
},
{
icon: '💳',
name: '创建支付记录',
desc: '初始化待支付状态',
status: 'success',
statusText: '成功'
},
{
icon: '🔄',
name: '事务提交',
desc: '所有操作原子性提交',
status: 'success',
statusText: '已提交'
}
]
},
{
name: '数据持久化',
layer: 'Repository',
code: `public interface OrderRepository extends JpaRepository&lt;Order, Long&gt; {
// 基本的 CRUD 已内置
}
// 实际执行:INSERT INTO orders (...) VALUES (...)`
}
subs: [
{ icon: '✅', name: '检查并扣减库存', desc: '确保库存充足', status: '成功' },
{ icon: '📝', name: '创建订单记录', desc: '生成订单主表', status: '成功' },
{ icon: '💳', name: '创建支付记录', desc: '初始化待支付', status: '成功' },
{ icon: '🔄', name: '事务提交', desc: '原子性提交', status: '已提交' }
] },
{ name: '数据持久化', layer: 'Repository',
code: `public interface OrderRepository extends JpaRepository<Order, Long> {
// 基本 CRUD 已内置
}` }
]
},
refund: {
title: '💰 退款处理流程',
description: '退款涉及订单状态变更、支付原路返回、库存回滚等操作',
title: '退款处理流程',
desc: '退款涉及订单状态变更、支付原路返回、库存回滚',
steps: [
{
name: '接收退款申请',
layer: 'Controller',
{ name: '接收退款申请', layer: 'Controller',
code: `@PostMapping("/orders/{orderId}/refund")
public ResponseEntity&lt;RefundDTO&gt; applyRefund(
@PathVariable Long orderId,
@RequestBody @Valid RefundRequest request
) {
RefundDTO refund = refundService.processRefund(orderId, request);
return ResponseEntity.ok(refund);
}`
},
{
name: '退款业务处理',
layer: 'Service',
code: `@Service
@Transactional
public class RefundService {
public RefundDTO processRefund(Long orderId, RefundRequest request) {
// 1. 验证订单状态
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException("订单不存在"));
if (order.getStatus() != OrderStatus.PAID) {
throw new InvalidOrderStateException("订单状态不允许退款");
}
// 2. 计算退款金额
BigDecimal refundAmount = calculateRefundAmount(order, request);
// 3. 调用支付渠道退款
PaymentRefundResult result = paymentService.refund(
order.getPaymentNo(),
refundAmount,
request.getReason()
);
// 4. 更新订单状态
order.setStatus(OrderStatus.REFUNDING);
orderRepository.save(order);
// 5. 保存退款记录
RefundRecord record = new RefundRecord();
record.setOrderId(orderId);
record.setAmount(refundAmount);
record.setReason(request.getReason());
record.setStatus(RefundStatus.PROCESSING);
refundRecordRepository.save(record);
// 6. 异步恢复库存
inventoryService.restoreStockAsync(order.getItems());
return convertToDTO(record);
}
public ResponseEntity<RefundDTO> applyRefund(
@PathVariable Long orderId, @RequestBody @Valid RefundRequest request) {
return ResponseEntity.ok(refundService.processRefund(orderId, request));
}` },
{ name: '退款业务处理', layer: 'Service',
code: `@Transactional
public RefundDTO processRefund(Long orderId, RefundRequest request) {
Order order = orderRepository.findById(orderId).orElseThrow();
if (order.getStatus() != OrderStatus.PAID)
throw new InvalidOrderStateException("不允许退款");
BigDecimal amount = calculateRefundAmount(order, request);
paymentService.refund(order.getPaymentNo(), amount, request.getReason());
order.setStatus(OrderStatus.REFUNDING);
orderRepository.save(order);
inventoryService.restoreStockAsync(order.getItems());
return convertToDTO(saveRefundRecord(orderId, amount, request));
}`,
subSteps: [
{ icon: '🔍', name: '验证订单状态', desc: '检查订单是否存在且可退款', status: 'success', statusText: '通过' },
{ icon: '💰', name: '计算退款金额', desc: '根据规则计算应退金额', status: 'success', statusText: '完成' },
{ icon: '🏦', name: '调用支付渠道', desc: '请求第三方支付退款', status: 'success', statusText: '处理中' },
{ icon: '📝', name: '更新订单状态', desc: '标记为退款中', status: 'success', statusText: '已更新' },
{ icon: '📊', name: '保存退款记录', desc: '记录退款流水', status: 'success', statusText: '已保存' },
{ icon: '🔄', name: '异步恢复库存', desc: '后台恢复商品库存', status: 'success', statusText: '已提交' }
]
}
subs: [
{ icon: '🔍', name: '验证订单状态', desc: '检查是否可退款', status: '通过' },
{ icon: '💰', name: '计算退款金额', desc: '根据规则计算', status: '完成' },
{ icon: '🏦', name: '调用支付渠道', desc: '请求第三方退款', status: '处理中' },
{ icon: '📝', name: '更新订单状态', desc: '标记为退款中', status: '已更新' },
{ icon: '🔄', name: '异步恢复库存', desc: '后台恢复库存', status: '已提交' }
] }
]
},
report: {
title: '📊 报表生成流程',
description: '复杂报表通常涉及多数据源查询、数据聚合计算、异步导出',
title: '报表生成流程',
desc: '复杂报表涉及多数据源查询、数据聚合、异步导出',
steps: [
{
name: '接收报表请求',
layer: 'Controller',
{ name: '接收报表请求', layer: 'Controller',
code: `@GetMapping("/reports/sales")
public ResponseEntity<ReportTaskDTO> generateSalesReport(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
LocalDate startDate,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
LocalDate endDate,
@RequestParam(required = false) List<Long> regionIds
) {
// 大报表采用异步生成
ReportTaskDTO task = reportService.createReportTask(
ReportType.SALES, startDate, endDate, regionIds
);
@RequestParam LocalDate startDate, @RequestParam LocalDate endDate) {
ReportTaskDTO task = reportService.createReportTask(startDate, endDate);
return ResponseEntity.accepted().body(task);
}`
},
{
name: '复杂报表业务编排',
layer: 'Service',
code: `@Service
public class ReportService {
@Async("reportExecutor")
public void generateReportAsync(Long taskId) {
ReportTask task = reportTaskRepository.findById(taskId)
.orElseThrow();
try {
task.setStatus(TaskStatus.RUNNING);
reportTaskRepository.save(task);
// 1. 从多个数据源聚合数据
SalesReportData data = aggregateSalesData(task);
// 2. 计算各项指标
calculateMetrics(data);
// 3. 生成图表数据
generateChartData(data);
// 4. 导出为 Excel
String fileUrl = exportToExcel(data, task);
task.setStatus(TaskStatus.COMPLETED);
task.setFileUrl(fileUrl);
task.setCompletedAt(LocalDateTime.now());
} catch (Exception e) {
task.setStatus(TaskStatus.FAILED);
task.setErrorMessage(e.getMessage());
}
reportTaskRepository.save(task);
}
private SalesReportData aggregateSalesData(ReportTask task) {
// 协调多个 Repository 查询数据
List<Order> orders = orderRepository
.findByCreatedAtBetween(task.getStartDate(), task.getEndDate());
List<Payment> payments = paymentRepository
.findByPaidAtBetween(task.getStartDate(), task.getEndDate());
List<RefundRecord> refunds = refundRecordRepository
.findByCreatedAtBetween(task.getStartDate(), task.getEndDate());
// 数据聚合逻辑...
return new SalesReportData(orders, payments, refunds);
}
}` },
{ name: '异步报表编排', layer: 'Service',
code: `@Async("reportExecutor")
public void generateReportAsync(Long taskId) {
ReportTask task = reportTaskRepository.findById(taskId).orElseThrow();
task.setStatus(TaskStatus.RUNNING);
reportTaskRepository.save(task);
SalesReportData data = aggregateSalesData(task);
calculateMetrics(data);
String fileUrl = exportToExcel(data, task);
task.setStatus(TaskStatus.COMPLETED);
task.setFileUrl(fileUrl);
reportTaskRepository.save(task);
}`,
subSteps: [
{ icon: '📥', name: '从多个数据源查询', desc: 'Orders/Payments/Refunds', status: 'success', statusText: '已查询' },
{ icon: '🔄', name: '数据聚合清洗', desc: '关联数据、处理缺失值', status: 'success', statusText: '已完成' },
{ icon: '📊', name: '计算业务指标', desc: 'GMV、订单数、客单价', status: 'success', statusText: '已计算' },
{ icon: '📈', name: '生成图表数据', desc: '趋势图、占比图数据结构', status: 'success', statusText: '已成' },
{ icon: '📄', name: '导出 Excel 文件', desc: '生成并上传至 OSS', status: 'success', statusText: '已完成' }
]
}
subs: [
{ icon: '📥', name: '数据源查询', desc: 'Orders/Payments/Refunds', status: '已查询' },
{ icon: '🔄', name: '数据聚合清洗', desc: '关联数据、处理缺失值', status: '已完成' },
{ icon: '📊', name: '计算业务指标', desc: 'GMV、订单数、客单价', status: '已计算' },
{ icon: '📄', name: '导出 Excel', desc: '生成并上传至 OSS', status: '已成' }
] }
]
}
}
const currentScenarioData = computed(() => scenarioData[currentScenario.value])
const data = computed(() => allData[current.value])
const toggleStep = (index) => {
const i = expandedSteps.value.indexOf(index)
if (i > -1) {
expandedSteps.value.splice(i, 1)
} else {
expandedSteps.value.push(index)
}
const toggleStep = (i) => {
const idx = expanded.value.indexOf(i)
if (idx > -1) expanded.value.splice(idx, 1)
else expanded.value.push(i)
}
const principles = [
{
id: 1,
icon: '🎯',
title: '单一职责',
desc: '一个 Service 类只负责一块业务领域',
example: 'UserService 只管用户,OrderService 只管订单'
},
{
id: 2,
icon: '🔄',
title: '事务边界',
desc: '在 Service 层声明式管理事务',
example: '@Transactional 放在 Service 方法上'
},
{
id: 3,
icon: '🔗',
title: '避免循环依赖',
desc: 'Service 之间不要互相调用',
example: 'A 调用 BB 又调用 A 会导致循环'
},
{
id: 4,
icon: '📦',
title: 'DTO 转换',
desc: '返回前转换为 DTO,不暴露实体',
example: 'return new UserDTO(user)'
}
{ title: '单一职责', desc: '一个 Service 只负责一块业务领域', example: 'UserService 只管用户,OrderService 只管订单' },
{ title: '事务边界', desc: '在 Service 层声明式管理事务', example: '@Transactional 放在 Service 方法上' },
{ title: '避免循环依赖', desc: 'Service 之间不要互相调用', example: 'A→B→A 会导致循环' },
{ title: 'DTO 转换', desc: '返回前转换为 DTO,不暴露实体', example: 'return new UserDTO(user)' }
]
</script>
<style scoped>
.service-layer-demo {
padding: 24px;
background: linear-gradient(135deg, #fff9f0 0%, #fff0e6 100%);
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.service-demo { padding: 20px; background: var(--vp-c-bg-soft); border-radius: 12px; }
.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; }
.demo-header {
text-align: center;
margin-bottom: 24px;
.tabs { display: flex; gap: 8px; margin-bottom: 16px; }
.tab {
padding: 7px 16px; border: 1px solid var(--vp-c-divider); background: var(--vp-c-bg);
border-radius: 6px; cursor: pointer; font-size: 13px; color: var(--vp-c-text-2); transition: all .2s;
}
.tab:hover { border-color: #f59e0b; color: #f59e0b; }
.tab.active { background: #f59e0b; border-color: #f59e0b; color: #fff; }
.demo-header h4 {
margin: 0 0 8px 0;
color: #1a1a2e;
font-size: 18px;
.flow-box {
padding: 18px; border-radius: 10px;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider); margin-bottom: 16px;
}
.flow-title { font-size: 15px; font-weight: 600; color: var(--vp-c-text-1); text-align: center; }
.flow-desc { font-size: 12px; color: var(--vp-c-text-3); text-align: center; margin: 4px 0 16px; }
.subtitle {
margin: 0;
color: #666;
font-size: 13px;
.steps { display: flex; flex-direction: column; gap: 10px; }
.step {
background: var(--vp-c-bg-soft); border-radius: 6px; border-left: 3px solid #f59e0b;
cursor: pointer; transition: all .2s; overflow: hidden;
}
.step:hover { transform: translateX(3px); }
.scenario-selector {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
padding: 16px;
background: white;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.selector-label {
font-weight: 600;
color: #303133;
font-size: 14px;
white-space: nowrap;
}
.scenario-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.scenario-btn {
padding: 8px 16px;
border: 1px solid #dcdfe6;
background: white;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
color: #606266;
transition: all 0.3s ease;
}
.scenario-btn:hover {
border-color: #e6a23c;
color: #e6a23c;
}
.scenario-btn.active {
background: #e6a23c;
border-color: #e6a23c;
color: white;
}
.flow-diagram {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.flow-header {
text-align: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
}
.flow-title {
display: block;
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 6px;
}
.flow-desc {
font-size: 13px;
color: #909399;
}
.flow-steps {
display: flex;
flex-direction: column;
gap: 12px;
}
.flow-step {
background: #f8f9fa;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
border-left: 4px solid #e6a23c;
}
.flow-step:hover {
background: #fff8f0;
transform: translateX(4px);
}
.step-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
}
.step-number {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: #e6a23c;
color: white;
border-radius: 50%;
font-size: 13px;
font-weight: 600;
}
.step-info {
flex: 1;
}
.step-name {
font-weight: 600;
color: #303133;
font-size: 14px;
}
.step-layer {
font-size: 12px;
color: #e6a23c;
margin-top: 2px;
}
.expand-icon {
color: #909399;
font-size: 12px;
.step-head { display: flex; align-items: center; gap: 10px; padding: 10px 14px; }
.step-num {
width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;
background: #f59e0b; color: #fff; border-radius: 50%; font-size: 12px; font-weight: 600; flex-shrink: 0;
}
.step-info { flex: 1; }
.step-name { font-weight: 600; font-size: 13px; color: var(--vp-c-text-1); }
.step-layer { font-size: 11px; color: #f59e0b; }
.expand { color: var(--vp-c-text-3); font-size: 11px; }
.step-code {
padding: 0 16px 16px 56px;
margin: 0 14px 14px 48px; padding: 10px; border-radius: 6px; overflow-x: auto;
background: var(--vp-code-block-bg); font-size: 11px; line-height: 1.5;
}
.step-code code { color: var(--vp-c-text-1); font-family: var(--vp-font-family-mono); }
.step-code pre {
margin: 0;
background: #2d2d2d;
padding: 12px;
border-radius: 6px;
overflow-x: auto;
.subs { padding: 0 14px 14px 48px; }
.sub {
display: flex; align-items: center; gap: 8px; padding: 8px 10px;
background: var(--vp-c-bg); border-radius: 6px; margin-bottom: 6px;
border-left: 2px solid var(--vp-c-green-1);
}
.sub-icon { font-size: 14px; }
.sub-info { flex: 1; }
.sub-name { font-size: 12px; font-weight: 500; color: var(--vp-c-text-1); }
.sub-desc { font-size: 11px; color: var(--vp-c-text-3); }
.sub-status { font-size: 10px; padding: 2px 6px; border-radius: 8px; background: var(--vp-c-green-soft); color: var(--vp-c-green-1); }
.step-code code {
color: #f8f8f2;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 11px;
line-height: 1.5;
.principles {
padding: 16px; border-radius: 10px;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
}
.sub-steps {
padding: 0 16px 16px 56px;
.principles-title { text-align: center; font-weight: 600; font-size: 14px; color: var(--vp-c-text-1); margin-bottom: 12px; }
.principle-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.principle {
padding: 12px; background: var(--vp-c-bg-soft); border-radius: 6px;
border-left: 3px solid #f59e0b;
}
.sub-step {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: white;
border-radius: 6px;
margin-bottom: 8px;
border-left: 3px solid #dcdfe6;
}
.sub-step.success {
border-left-color: #67c23a;
background: #f6ffed;
}
.sub-step-icon {
font-size: 16px;
}
.sub-step-content {
flex: 1;
}
.sub-step-name {
font-size: 13px;
font-weight: 500;
color: #303133;
}
.sub-step-desc {
font-size: 11px;
color: #909399;
margin-top: 2px;
}
.sub-step-status {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
background: #f0f9ff;
color: #1890ff;
}
.sub-step.success .sub-step-status {
background: #f6ffed;
color: #52c41a;
}
.design-principles {
margin-top: 24px;
padding: 20px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.design-principles h5 {
margin: 0 0 16px 0;
color: #1a1a2e;
font-size: 15px;
}
.principles-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.principle-card {
padding: 14px;
background: #f8f9fa;
border-radius: 6px;
border-left: 3px solid #e6a23c;
}
.principle-icon {
font-size: 20px;
margin-bottom: 6px;
}
.principle-title {
font-weight: 600;
color: #303133;
font-size: 13px;
margin-bottom: 4px;
}
.principle-desc {
color: #606266;
font-size: 11px;
margin-bottom: 8px;
line-height: 1.5;
}
.principle-example {
background: #2d2d2d;
padding: 8px;
border-radius: 4px;
overflow-x: auto;
}
.principle-example code {
color: #f8f8f2;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 10px;
line-height: 1.4;
.p-title { font-weight: 600; font-size: 13px; color: var(--vp-c-text-1); margin-bottom: 4px; }
.p-desc { font-size: 11px; color: var(--vp-c-text-2); margin-bottom: 6px; }
.p-example {
display: block; padding: 6px; border-radius: 4px; overflow-x: auto;
background: var(--vp-code-block-bg); font-size: 10px; color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
}
@media (max-width: 768px) {
.principles-grid {
grid-template-columns: 1fr;
}
.scenario-selector {
flex-direction: column;
align-items: flex-start;
}
.step-code {
padding-left: 16px;
}
.principle-grid { grid-template-columns: 1fr; }
.tabs { flex-wrap: wrap; }
.step-code { margin-left: 14px; }
}
</style>
@@ -47,12 +47,17 @@
</template>
<script setup>
import { ref, onUnmounted } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
const selected = ref(0)
const visibleSteps = ref(0)
let timer = null
onMounted(() => {
// 组件挂载后开始动画,避免模块加载时启动定时器导致 build 卡住
selectMode(0)
})
onUnmounted(() => {
if (timer) clearInterval(timer)
})
@@ -117,8 +122,6 @@ const modes = [
langs: ['JavaScript (V8)', 'Java (JVM)', 'C# (.NET)']
}
]
selectMode(0)
</script>
<style scoped>
@@ -65,7 +65,7 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
const algo = ref('token')
const totalSent = ref(0)
@@ -105,9 +105,19 @@ function reset() {
recentRequests.value = []
windowRequests.value = []
if (tokenInterval) clearInterval(tokenInterval)
startTokenRefill()
// 只在令牌桶模式下启动补充
if (algo.value === 'token') startTokenRefill()
}
onMounted(() => {
// 组件挂载后启动令牌补充,避免模块加载时启动定时器导致 build 卡住
startTokenRefill()
})
onUnmounted(() => {
if (tokenInterval) clearInterval(tokenInterval)
})
function checkLimit() {
if (algo.value === 'token') {
if (tokens.value > 0) { tokens.value--; return true }
@@ -156,8 +166,6 @@ async function sendBurst() {
await new Promise(r => setTimeout(r, 100))
}
}
startTokenRefill()
</script>
<style scoped>
+6
View File
@@ -517,6 +517,7 @@ import GoroutineGreenThreadDemo from './components/appendix/concurrency-models/G
import DeveloperEfficiencyDemo from './components/appendix/backend-languages/DeveloperEfficiencyDemo.vue'
import LanguageEcosystemDemo from './components/appendix/backend-languages/LanguageEcosystemDemo.vue'
import MemoryManagementDemo from './components/appendix/backend-languages/MemoryManagementDemo.vue'
import LanguageScopeDemo from './components/appendix/backend-languages/LanguageScopeDemo.vue'
// Component State Management Components
import ComponentHierarchyDemo from './components/appendix/component-state-management/ComponentHierarchyDemo.vue'
@@ -602,6 +603,8 @@ import ServiceLayerDemo from './components/appendix/backend-layered-architecture
import RepositoryLayerDemo from './components/appendix/backend-layered-architecture/RepositoryLayerDemo.vue'
import DomainModelDemo from './components/appendix/backend-layered-architecture/DomainModelDemo.vue'
import DtoFlowDemo from './components/appendix/backend-layered-architecture/DtoFlowDemo.vue'
import DependencyDirectionDemo from './components/appendix/backend-layered-architecture/DependencyDirectionDemo.vue'
import CleanArchitectureDemo from './components/appendix/backend-layered-architecture/CleanArchitectureDemo.vue'
// Browser Rendering Pipeline Components
import DomToRenderTreeDemo from './components/appendix/browser-rendering-pipeline/DomToRenderTreeDemo.vue'
@@ -1339,6 +1342,7 @@ export default {
app.component('DeveloperEfficiencyDemo', DeveloperEfficiencyDemo)
app.component('LanguageEcosystemDemo', LanguageEcosystemDemo)
app.component('MemoryManagementDemo', MemoryManagementDemo)
app.component('LanguageScopeDemo', LanguageScopeDemo)
// Concurrency Models Components Registration
app.component('ProcessThreadCoroutineDemo', ProcessThreadCoroutineDemo)
@@ -1437,6 +1441,8 @@ export default {
app.component('RepositoryLayerDemo', RepositoryLayerDemo)
app.component('DomainModelDemo', DomainModelDemo)
app.component('DtoFlowDemo', DtoFlowDemo)
app.component('DependencyDirectionDemo', DependencyDirectionDemo)
app.component('CleanArchitectureDemo', CleanArchitectureDemo)
// Browser Rendering Pipeline Components Registration
app.component('DomToRenderTreeDemo', DomToRenderTreeDemo)
@@ -5,7 +5,7 @@ export default {
output: '预期产出',
assignment: '课后任务'
},
'en': {
en: {
title: 'Learning Objectives',
duration: 'Estimated Time',
output: 'Expected Output',
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -55,7 +55,7 @@
| 前端三:参考 UI 设计规范与多产品 UI 设计 | 围绕统一主视觉扩展多产品界面,练习系统化设计能力 | 🚧 |
| [前端四:一起做霍格沃茨画像](/zh-cn/stage-2/frontend/2.4-hogwarts-portraits/) | 从 0 到 1 做出接入 AI 能力的前端应用,串联设计与开发 | 🚧 |
#### 后端与全栈部分
#### 后端开发部分
| 章节 | 关键内容 | 状态 |
| :---------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------ | :--- |
@@ -94,7 +94,20 @@ AI 训练数据中有海量的前端代码,而大部分代码都使用一些"
同样的需求,第二个提示词能让 AI 生成一个风格鲜明、令人印象深刻的页面。
### 2.3 三款常用风格模板
### 2.3 前端美化 Skills 资源库
不要从零开始写提示词!这里收集了与前端美化直接相关的 AI Skills:
| 仓库名 | 内容 | Star | 链接 |
|:---|:---|:---|:---|
| **ui-ux-pro-max-skill** | 57种风格 + 95种配色 + 56种字体 | 10k+ | [GitHub](https://github.com/nextlevelbuilder/ui-ux-pro-max-skill) |
| **antigravity-awesome-skills** | 避免通用 AI 审美套路 | - | [GitHub](https://github.com/sickn33/antigravity-awesome-skills) |
| **superdesigndev/superdesign** | AI 原生 UI 开发工具 | 4.7k | [GitHub](https://github.com/superdesigndev/superdesign) |
| **anthropics/skills/frontend-design** | Anthropic 官方前端设计 Skill | - | [GitHub](https://github.com/anthropics/skills) |
> 💡 更多风格提示词请参考[附录:设计风格提示词速查](#style-prompts)
### 2.5 三款常用风格模板
这里给你三款经过验证的风格模板,直接复制修改使用:
@@ -480,3 +493,21 @@ npx skills add anthropics/skills/brand-guidelines
| **浆果** | #8B5CF6 | #EC4899 | #FAF5FF | 浪漫、创意 |
| **咖啡** | #78350F | #D97706 | #FFFBEB | 温暖、复古 |
| **单石** | #6B7280 | #9CA3AF | #F9FAFB | 专业、中性 |
## 附录:设计风格提示词速查 {#style-prompts}
让前端页面更好看可以尝试的提示词:
### 风格类别
| 风格 | 关键词(英文) | 核心视觉特征 | 提示词示例 |
|:---|:---|:---|:---|
| **波普艺术** | Pop Art | 大胆的撞色、黑色轮廓线、网点纹理 | Pop art style website, bold colors and comic dots, vibrant |
| **极简主义** | Minimalism | 大量留白、极少色彩与线条、无装饰 | Minimalist web design, ample white space, geometric, serene |
| **抽象表现主义** | Abstract Expressionism | 充满情感张力的笔触、泼洒色彩 | Abstract expressionism background, dynamic paint splashes, emotional |
| **复古风格** | Retro/Vintage | 旧式字体、做旧纹理、复古配色 | Retro 80s website design, neon grid and synthwave color palette |
| **赛博朋克** | Cyberpunk | 高对比霓虹色、故障艺术效果、暗黑背景 | Cyberpunk UI, neon lights on dark background, glitch effects |
| **新拟态** | Neumorphism | 柔和的阴影与高光,轻微凸起/凹陷质感 | Neumorphism design style, soft shadows, clean and modern |
| **生成式艺术** | Generative Art | 算法生成的流动的视觉图案 | Generative art background, flowing algorithmic patterns, digital |
| **酸性设计** | Acid Graphics | 金属质感、玻璃态、锯齿字体 | Acid graphics web layout, glass morphism, chaotic typography |
| **沉浸式3D** | Immersive 3D | 互动3D场景、空间感极强 | Immersive 3D website, interactive product model in space |
@@ -1,441 +0,0 @@
# 使用现代组件库更新你的界面
在前面的课程中,你已经学会了如何用设计工具画出界面、用 AI IDE 把设计稿变成代码,甚至完成了一个完整的前端项目。但你可能也发现了一个问题:自己从零写出来的按钮、表单、弹窗,虽然能用,但总觉得和"专业产品"差了点意思——样式不够统一、交互细节不够丝滑、适配不同屏幕也很头疼。
这就是**组件库**要解决的问题。
组件库是一套预先设计好、开发好的 UI 零件集合。按钮、输入框、下拉菜单、对话框、表格……这些你在任何产品中都会反复用到的界面元素,组件库已经帮你做好了,而且经过了大量用户的验证和打磨。你只需要像搭积木一样把它们组合起来,就能快速构建出专业级的界面。
## 你将学到
1. 理解什么是前端组件库,以及为什么现代开发几乎都在用它
2. 认识四个最具代表性的组件库,了解它们各自擅长的场景
3. 通过三个实战场景(落地页、产品页面、后台管理),学会用 AI IDE + 组件库进行 Vibe Coding
4. 学会阅读组件库文档,根据需求找到合适的组件并正确使用
## 1. 为什么需要组件库?
想象你在装修房子。你可以自己从木头开始做一把椅子,但更常见的做法是去宜家买一把——设计好看、质量稳定、说明书清晰,拿回家组装就行。
组件库就是前端开发中的"宜家"。它提供的不是家具,而是界面零件:
| 自己手写 | 使用组件库 |
| :--- | :--- |
| 需要自己处理样式、交互、动画 | 开箱即用,样式和交互已经打磨好 |
| 不同页面的按钮可能长得不一样 | 全局风格统一,自动保持一致性 |
| 适配手机、平板需要额外工作 | 大多数组件库已内置响应式支持 |
| 无障碍访问(Accessibility)容易遗漏 | 专业组件库已处理好键盘导航、屏幕阅读器等 |
| 开发速度慢 | 开发速度快,专注业务逻辑 |
简单来说:**组件库让你把时间花在"做什么"上,而不是"怎么画"上。**
### 眼见为实:同一个需求,加不加组件库的差距
光说不练没有说服力。我们在 Trae 中用几乎相同的需求,分别不指定和指定组件库,看看生成结果的差距。
**提示词一:不使用组件库**
```text
请帮我用 React + TypeScript 生成一个 AI 写作助手的数据仪表盘页面,包含:
- 顶部标题栏和导出按钮
- 四张统计卡片(用户数、活跃用户、文档数、收入),显示数值和涨跌趋势
- 一个折线图和一个饼图
- 一个用户列表表格,带分页
- 左侧导航侧边栏
```
在 Trae 中直接运行后的效果:
<!-- TODO: 替换为 Trae 中不使用组件库生成的仪表盘截图 -->
<!-- ![Trae 生成的仪表盘(不使用组件库)](images/compare-without-lib.png) -->
**提示词二:使用 shadcn/ui 组件库**
```text
请帮我用 React + TypeScript + shadcn/ui + Tailwind CSS 生成一个 AI 写作助手的数据仪表盘页面,包含:
- 顶部标题栏和导出按钮
- 四张统计卡片(用户数、活跃用户、文档数、收入),显示数值和涨跌趋势
- 一个折线图和一个饼图
- 一个用户列表表格,带分页
- 左侧导航侧边栏
```
同样在 Trae 中直接运行后的效果:
<!-- TODO: 替换为 Trae 中使用 shadcn/ui 生成的仪表盘截图 -->
<!-- ![Trae 生成的仪表盘(使用 shadcn/ui)](images/compare-with-lib.png) -->
同样的需求,唯一的区别只是在提示词开头加上了 `shadcn/ui + Tailwind CSS`,Trae 生成的结果在视觉一致性、交互细节、整体打磨程度上就完全不在一个层级。这就是组件库带来的"免费升级"——你只需要在提示词里多写一个组件库的名字。
## 2. 认识四个核心组件库
组件库数量众多(完整列表见[附录](#附录-更多组件库一览)),但你只需要先认识这四个最具代表性的:
| 组件库 | 框架 | 一句话定位 | 官网 |
| :--- | :--- | :--- | :--- |
| [Ant Design](https://ant.design) | React | 蚂蚁集团出品,企业级中后台的事实标准,组件覆盖面极广 | ant.design |
| [shadcn/ui](https://ui.shadcn.com) | React | 不装 npm 包,直接把代码复制到你项目里,基于 Tailwind CSS,定制自由度最高 | ui.shadcn.com |
| [HeroUI](https://heroui.com)(原 NextUI | React | 默认样式精美、动画流畅,适合对视觉品质有要求的落地页和产品展示 | heroui.com |
| [Material UI](https://mui.com) | React | 最老牌的 React 组件库,实现 Google Material Design 规范,生态最成熟 | mui.com |
> Vue 用户同样有丰富选择:[Element Plus](https://element-plus.org)(国内最流行)、[Ant Design Vue](https://antdv.com)、[Naive UI](https://www.naiveui.com) 等,详见[附录](#附录-更多组件库一览)。
不同组件库擅长不同场景。接下来我们通过三个真实开发场景,带你体验如何用 AI IDE + 组件库进行 Vibe Coding。
为了展示不同组件库的风格和特点,我们在每个场景中刻意选用了不同的库。但请注意:**这只是为了让你多见识几种方案**,实际开发中你完全可以只用自己最顺手的那一个。比如你喜欢 shadcn/ui 的风格,用它做落地页、产品页、后台管理都没问题。选一个你觉得好看、用着舒服的,比什么都重要。
## 3. 实战一:用 HeroUI 构建产品落地页
**场景**:你做了一个 AI 写作助手产品,需要一个漂亮的落地页来展示产品特性、吸引用户注册。落地页需要视觉冲击力强、动画流畅、在手机上也好看。
**为什么选 HeroUI**:HeroUI 的默认样式就很精美,自带流畅的过渡动画,非常适合面向用户的展示型页面。
### 3.1 创建项目
```bash
# 使用 HeroUI 官方 CLI 创建项目
npx create-heroui-app@latest ai-writer-landing
cd ai-writer-landing
npm install
```
<!-- TODO: 替换为 HeroUI 官网首页或组件展示截图 -->
<!-- ![HeroUI 组件库官网](images/heroui-homepage.png) -->
### 3.2 用 AI IDE 生成落地页
打开 AI IDECursor、Trae 等),在对话框中输入:
```text
我正在用 HeroUI (React) 构建一个 AI 写作助手的落地页,请帮我实现以下结构:
1. 顶部导航栏:左侧 Logo + 产品名,右侧"功能"、"定价"、"关于"导航链接 + "开始使用"按钮
2. Hero 区域:大标题"让 AI 成为你的写作搭档",副标题描述产品价值,一个"免费试用"主按钮和一个"查看演示"次按钮,下方放一张产品截图
3. 功能展示区:3 列卡片布局,分别展示"智能续写"、"风格调整"、"多语言翻译"三个核心功能,每张卡片有图标、标题、描述
4. 定价区域:3 个定价卡片(免费版、专业版、团队版),专业版高亮推荐
5. 底部 CTA:一句号召性文案 + 注册按钮
6. Footer:版权信息 + 社交媒体链接
请使用 HeroUI 组件库。
页面要有现代感,支持暗色模式,移动端响应式。
```
<!-- TODO: 替换为 AI IDE 生成落地页的过程截图或生成结果截图 -->
<!-- ![AI 生成的 HeroUI 落地页效果](images/heroui-landing-result.png) -->
### 3.3 AI 会用到的关键组件
AI 生成的代码中,你会看到这些 HeroUI 组件:
```jsx
import {
Navbar, NavbarBrand, NavbarContent, NavbarItem,
Button,
Card, CardHeader, CardBody, CardFooter,
Divider,
Link,
Chip
} from '@heroui/react'
```
每个组件的作用:
| 组件 | 用途 | 落地页中的位置 |
| :--- | :--- | :--- |
| `Navbar` | 顶部导航栏 | 页面最顶部,固定不动 |
| `Button` | 按钮,支持多种变体和颜色 | CTA 按钮、导航按钮 |
| `Card` | 卡片容器 | 功能展示、定价卡片 |
| `Chip` | 小标签 | "推荐"、"最受欢迎"标记 |
| `Divider` | 分割线 | 区域之间的视觉分隔 |
### 3.4 迭代优化
生成的初版代码可能不完全满意,继续和 AI 对话调整:
```text
请帮我做以下调整:
1. Hero 区域的标题加上渐变色效果(从蓝色到紫色)
2. 功能卡片加上 hover 时的上浮动画
3. 定价区域的"专业版"卡片加上边框高亮和"最受欢迎"标签
4. 移动端导航改为汉堡菜单
```
<!-- TODO: 替换为迭代优化后的落地页效果截图 -->
<!-- ![迭代优化后的落地页](images/heroui-landing-iterated.png) -->
> **Vibe Coding 的核心**:你不需要记住每个组件的 API,只需要用自然语言描述你想要的效果,AI 会帮你找到合适的组件和写法。遇到不满意的地方,继续对话迭代就好。
## 4. 实战二:用 shadcn/ui 构建产品页面
**场景**:你的 AI 写作助手需要一个用户登录后的主界面——左侧是文档列表,右侧是编辑器,顶部有工具栏。这是一个功能型产品页面,需要高度定制化的 UI。
**为什么选 shadcn/ui**shadcn/ui 把组件代码直接放进你的项目,你可以随意修改任何细节。对于需要深度定制的产品界面,这种"拥有代码"的模式最灵活。
<!-- TODO: 替换为 shadcn/ui 官网或组件展示截图 -->
<!-- ![shadcn/ui 组件库官网](images/shadcn-homepage.png) -->
### 4.1 创建项目
```bash
# 创建 Next.js 项目
npx create-next-app@latest ai-writer-app --typescript --tailwind --app
cd ai-writer-app
# 初始化 shadcn/ui
npx shadcn@latest init
# 按需添加组件(不是一次性安装所有组件)
npx shadcn@latest add button card input sidebar sheet dialog
```
shadcn/ui 的独特之处:每次 `add` 一个组件,它会把源代码复制到你项目的 `components/ui/` 目录下。你可以直接打开这些文件修改样式和行为。
### 4.2 用 AI IDE 生成产品界面
```text
我正在用 Next.js + shadcn/ui + Tailwind CSS 构建一个 AI 写作助手的主界面,请帮我实现:
整体布局:
- 左侧可折叠侧边栏(宽 280px):顶部是"新建文档"按钮,下方是文档列表,每个文档显示标题和最后编辑时间,支持右键菜单(重命名、删除)
- 右侧主区域分为上下两部分:
- 顶部工具栏:文档标题(可编辑)、字数统计、"AI 续写"按钮、"导出"下拉菜单
- 下方编辑区域:一个大的文本输入区域,占满剩余空间
交互细节:
- 点击"AI 续写"按钮后,按钮变为加载状态,编辑器底部出现 AI 生成的文本(带打字机效果)
- 侧边栏在移动端变为抽屉式弹出
- 文档列表当前选中项高亮
请使用 shadcn/ui 组件库。
```
<!-- TODO: 替换为 AI 生成的 shadcn/ui 产品界面截图 -->
<!-- ![AI 生成的 shadcn/ui 产品页面效果](images/shadcn-product-result.png) -->
### 4.3 AI 会用到的关键组件
```tsx
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader } from '@/components/ui/card'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import {
Sheet,
SheetContent,
SheetTrigger
} from '@/components/ui/sheet'
import {
Sidebar,
SidebarContent,
SidebarHeader
} from '@/components/ui/sidebar'
```
| 组件 | 用途 | 产品页面中的位置 |
| :--- | :--- | :--- |
| `Sidebar` | 可折叠侧边栏 | 左侧文档列表 |
| `Sheet` | 移动端抽屉 | 移动端侧边栏替代 |
| `DropdownMenu` | 下拉菜单 | "导出"按钮、右键菜单 |
| `Dialog` | 对话框 | 重命名、删除确认 |
| `Button` | 按钮,支持 variant 和 loading | 各种操作按钮 |
| `Input` | 输入框 | 文档标题编辑 |
### 4.4 定制组件样式
shadcn/ui 的优势在于你可以直接修改组件源码。比如你想让按钮的圆角更大:
```text
请帮我修改 components/ui/button.tsx
把所有按钮的默认圆角从 rounded-md 改为 rounded-xl
并给 primary 变体加上微妙的阴影效果
```
AI 会直接修改你项目中的组件文件,而不是覆盖 npm 包的样式——这就是 shadcn/ui "拥有代码"的好处。
<!-- TODO: 替换为 shadcn/ui 组件源码在项目中的截图,展示可直接编辑 -->
<!-- ![shadcn/ui 组件代码在项目中可直接编辑](images/shadcn-code-ownership.png) -->
## 5. 实战三:用 Ant Design 构建后台管理界面
**场景**:你的 AI 写作助手上线后,需要一个管理后台来查看用户数据、管理文档内容、处理付费订单。后台管理系统的核心是数据展示和操作效率。
**为什么选 Ant Design**Ant Design 在中后台领域积累最深,表格、表单、图表等业务组件开箱即用,内置了大量企业级交互模式(批量操作、高级筛选、数据导出等)。
<!-- TODO: 替换为 Ant Design 官网或 Pro Components 展示截图 -->
<!-- ![Ant Design 组件库官网](images/antd-homepage.png) -->
### 5.1 创建项目
```bash
# 使用 Ant Design Pro 脚手架(内置布局、路由、权限)
npx create-umi@latest ai-writer-admin
# 选择 Ant Design Pro 模板
cd ai-writer-admin
npm install
```
或者从零开始:
```bash
npx create-react-app ai-writer-admin --template typescript
cd ai-writer-admin
npm install antd @ant-design/icons @ant-design/pro-components
```
### 5.2 用 AI IDE 生成管理后台
```text
我正在用 React + Ant Design 组件库构建 AI 写作助手的管理后台,请帮我实现用户管理页面:
页面布局:
- 整体框架为左侧菜单 + 顶部面包屑
- 左侧菜单项:仪表盘、用户管理、文档管理、订单管理、系统设置
用户管理页面内容:
- 顶部统计卡片:总用户数、今日新增、活跃用户数、付费用户数
- 搜索筛选区:用户名搜索、注册时间范围、用户状态下拉筛选、"搜索"和"重置"按钮
- 用户表格:
- 列:头像、用户名、邮箱、注册时间、订阅计划(用彩色标签区分)、状态(用徽标显示)、操作
- 支持分页(每页 20 条)
- 支持批量选择 + 批量操作(禁用、导出)
- 操作列:查看详情、编辑、禁用(需要二次确认)
- 点击"查看详情"弹出侧边抽屉,显示用户完整信息和最近文档列表
请使用 Ant Design 组件库。
```
<!-- TODO: 替换为 AI 生成的 Ant Design 后台管理界面截图 -->
<!-- ![AI 生成的 Ant Design 后台管理界面](images/antd-admin-result.png) -->
### 5.3 AI 会用到的关键组件
```tsx
import { PageContainer, ProLayout } from '@ant-design/pro-components'
import { ProTable } from '@ant-design/pro-components'
import { StatisticCard } from '@ant-design/pro-components'
import {
Button, Tag, Badge, Space, Drawer,
Popconfirm, message, Modal
} from 'antd'
import {
UserOutlined, SearchOutlined, ExportOutlined
} from '@ant-design/icons'
```
| 组件 | 用途 | 后台中的位置 |
| :--- | :--- | :--- |
| `ProLayout` | 后台整体布局框架 | 页面骨架(菜单 + 内容区) |
| `ProTable` | 高级表格,内置搜索、分页、列设置 | 用户列表、文档列表、订单列表 |
| `StatisticCard` | 数据统计卡片 | 仪表盘、页面顶部概览 |
| `Tag` / `Badge` | 状态标签 | 订阅计划、用户状态 |
| `Drawer` | 侧边抽屉 | 用户详情、编辑表单 |
| `Popconfirm` | 气泡确认框 | 删除、禁用等危险操作 |
### 5.4 继续迭代:添加仪表盘
```text
请帮我实现仪表盘页面:
1. 顶部 4 个统计卡片(总用户、总文档、今日 API 调用次数、月收入),每个卡片显示数值和环比变化
2. 中间一行两列:
- 左侧:最近 7 天的用户增长折线图(用 @ant-design/charts
- 右侧:订阅计划分布饼图
3. 底部:最近操作日志表格(时间、用户、操作类型、详情)
使用 Ant Design 的 Card、Row、Col 布局,图表用 @ant-design/charts
```
<!-- TODO: 替换为仪表盘页面效果截图 -->
<!-- ![Ant Design 仪表盘页面效果](images/antd-dashboard-result.png) -->
> **后台管理的 Vibe Coding 技巧**:后台页面结构相对固定(表格 + 搜索 + 弹窗),非常适合用 AI 批量生成。你可以先让 AI 生成一个"用户管理"页面作为模板,然后说"参考用户管理页面的结构,帮我生成文档管理页面",AI 会复用相同的布局模式。
## 6. 学会查文档:组件库的"说明书"
Vibe Coding 中 AI 会帮你写大部分代码,但当 AI 生成的结果不对、或者你想微调某个组件的行为时,**查文档**是最快的解决方式。
以 Ant Design 为例,它的文档地址是:`https://ant.design/components/overview-cn`
查文档的标准流程:
1. **明确需求**:比如"我需要表格支持行选择"
2. **在文档中搜索**:搜索"Table"进入表格组件页面
3. **查看示例**:文档中每个组件都有多个在线示例,找到"可选择"示例
4. **复制代码**:把示例代码复制到你的项目中
5. **查看 API 表格**:在页面底部找到 `rowSelection` 属性的完整配置项
> 你也可以把文档链接直接发给 AI IDE"请参考 https://ant.design/components/table-cn 的 rowSelection API,帮我给用户表格加上批量选择功能"。给 AI 提供文档链接,生成的代码会更准确。
各组件库的文档地址速查:
| 组件库 | 文档地址 |
| :--- | :--- |
| Ant Design | `https://ant.design/components/overview-cn` |
| shadcn/ui | `https://ui.shadcn.com/docs/components` |
| HeroUI | `https://heroui.com/docs/components` |
| Material UI | `https://mui.com/material-ui/all-components/` |
| Element Plus | `https://element-plus.org/zh-CN/component/overview.html` |
## 7. 小结
三个实战场景覆盖了最常见的前端开发需求:
| 场景 | 推荐组件库 | 核心特点 |
| :--- | :--- | :--- |
| 落地页 / 展示页 | HeroUI | 默认样式精美,动画流畅,视觉冲击力强 |
| 产品功能页面 | shadcn/ui | 代码完全可控,深度定制灵活 |
| 后台管理系统 | Ant Design | 业务组件丰富,表格表单开箱即用 |
Vibe Coding 的工作流总结:
1. 根据场景选择合适的组件库
2. 用 AI IDE 描述你想要的页面结构和交互
3. AI 生成初版代码,你预览效果
4. 用自然语言继续迭代调整
5. 遇到细节问题时查阅组件库文档
### 练习
选择以下任一场景,用 AI IDE + 组件库从零完成:
1. 用 HeroUI 为你之前做的项目(比如霍格沃茨画像)做一个展示落地页
2. 用 shadcn/ui 构建一个笔记应用的主界面(侧边栏 + 编辑器)
3. 用 Ant Design 构建一个简单的内容管理后台(文章列表 + 新建文章表单)
---
## 附录:更多组件库一览
除了正文介绍的四个核心库,前端生态中还有大量优秀的组件库。下面按框架分类列出,方便你根据项目需求选择。
### Vue 生态
| 组件库 | Stars | 简介 | 适用场景 |
| :--- | :--- | :--- | :--- |
| [Element Plus](https://element-plus.org) | ~27k | 饿了么团队打造的 Vue 3 企业级组件库,国内使用最广泛,中文生态极佳 | 中后台管理系统 |
| [Vuetify](https://vuetifyjs.com) | ~41k | 最流行的 Vue Material Design 组件库,80+ 组件,文档完善 | Google 设计风格项目 |
| [Ant Design Vue](https://antdv.com) | ~21k | 基于蚂蚁设计体系的 Vue 3 组件库,设计规范统一 | 企业级中后台 |
| [Naive UI](https://www.naiveui.com) | ~18k | TypeScript 编写,主题定制性极强,不依赖 CSS 预处理器 | 对设计有独特要求的项目 |
| [Quasar](https://quasar.dev) | ~27k | 一套代码构建 SPA、SSR、PWA、移动端和桌面端应用 | 跨平台项目 |
| [Vant](https://vant-ui.github.io/vant) | ~24k | 有赞团队开发的轻量级移动端组件库,覆盖电商常见需求 | 移动端 H5 页面 |
| [PrimeVue](https://primevue.org) | ~14k | 90+ 组件,支持多种主题(Material、Bootstrap 等) | 需要丰富组件和多主题 |
| [Arco Design Vue](https://arco.design/vue) | ~3k | 字节跳动出品,组件质量高,内置暗色模式 | 中后台产品 |
| [TDesign Vue Next](https://tdesign.tencent.com/vue-next) | ~2k | 腾讯出品,设计语言统一,覆盖桌面端常用场景 | 腾讯生态或企业级项目 |
### React 生态
| 组件库 | Stars | 简介 | 适用场景 |
| :--- | :--- | :--- | :--- |
| [Material UI (MUI)](https://mui.com) | ~95k | Google Material Design 规范的老牌实现,组件最全面,生态最成熟 | 快速构建企业级应用 |
| [Ant Design](https://ant.design) | ~94k | 蚂蚁集团出品,内置大量高质量业务组件,中文开发者社区主导地位 | 企业级中后台 |
| [shadcn/ui](https://ui.shadcn.com) | ~83k | 代码复制到项目中而非 npm 安装,基于 Radix UI + Tailwind CSS,完全可控 | 需要高度定制的项目 |
| [Chakra UI](https://chakra-ui.com) | ~39k | 以开发体验为核心,API 简洁,内置无障碍访问支持 | 快速原型开发 |
| [Mantine](https://mantine.dev) | ~28k | 100+ 组件和 50+ hooks,涵盖日期选择器、富文本编辑器等高级组件 | 需要开箱即用的全功能方案 |
| [Headless UI](https://headlessui.com) | ~27k | Tailwind Labs 官方出品的无样式组件库,同时支持 React 和 Vue | 搭配 Tailwind CSS 使用 |
| [HeroUI](https://heroui.com) | ~24k | 基于 Tailwind CSS + React Aria,默认样式精美,动画流畅 | 追求视觉品质的项目 |
| [Radix UI](https://www.radix-ui.com) | ~17k | 无样式底层组件原语库,专注无障碍和组件行为,是 shadcn/ui 的底层基础 | 构建自定义设计系统 |
+1 -1
View File
@@ -31,7 +31,7 @@
/>
</NavGrid>
### 后端与全栈
### 后端开发
学习 API 设计、数据库管理以及应用部署策略: