chore: save local history restorations from accidental git restore

This commit is contained in:
sanbuphy
2026-02-23 01:40:56 +08:00
parent 780be69b99
commit 2a0fdd3392
27 changed files with 5971 additions and 2743 deletions
@@ -0,0 +1,479 @@
<template>
<div class="demo">
<div class="header">
<span class="icon">🎨</span>
<span class="title">四种 API 风格对比</span>
</div>
<div class="tabs">
<button
v-for="style in styles"
:key="style.id"
:class="['tab', { active: active === style.id }]"
@click="active = style.id"
>
{{ style.icon }} {{ style.name }}
</button>
</div>
<div class="content">
<div class="style-header">
<h4>{{ currentStyle.name }}</h4>
<span class="badge">{{ currentStyle.badge }}</span>
</div>
<p class="desc">{{ currentStyle.desc }}</p>
<div class="example-section">
<div class="example-label">示例获取用户信息</div>
<pre class="code-block"><code>{{ currentStyle.example }}</code></pre>
</div>
<div class="features">
<div class="features-title">核心特点</div>
<div class="features-grid">
<div
v-for="(f, i) in currentStyle.features"
:key="i"
class="feature-item"
>
<span class="check"></span>
<span>{{ f }}</span>
</div>
</div>
</div>
<div class="meta">
<div class="meta-row">
<span class="meta-label">适用场景</span>
<span class="meta-value">{{ currentStyle.scenarios }}</span>
</div>
<div class="meta-row">
<span class="meta-label">官方地址</span>
<a :href="currentStyle.official" target="_blank" class="meta-link">{{
currentStyle.official
}}</a>
</div>
</div>
</div>
<div class="compare-section">
<div class="compare-title">📊 风格速览对比</div>
<div class="compare-table">
<div class="compare-row head">
<div class="cell">特性</div>
<div class="cell">RPC</div>
<div class="cell highlight">REST</div>
<div class="cell">GraphQL</div>
<div class="cell">gRPC</div>
</div>
<div class="compare-row">
<div class="cell">核心理念</div>
<div class="cell">面向过程</div>
<div class="cell highlight">面向资源</div>
<div class="cell">面向数据</div>
<div class="cell">面向方法</div>
</div>
<div class="compare-row">
<div class="cell">URL 风格</div>
<div class="cell">动词为主</div>
<div class="cell highlight">名词为主</div>
<div class="cell">单一端点</div>
<div class="cell">不依赖URL</div>
</div>
<div class="compare-row">
<div class="cell">学习曲线</div>
<div class="cell low"></div>
<div class="cell"></div>
<div class="cell"></div>
<div class="cell high"></div>
</div>
<div class="compare-row">
<div class="cell">性能</div>
<div class="cell">一般</div>
<div class="cell">一般</div>
<div class="cell">较好</div>
<div class="cell best">优秀</div>
</div>
<div class="compare-row">
<div class="cell">使用占比</div>
<div class="cell">~30%</div>
<div class="cell highlight">~50%</div>
<div class="cell">~15%</div>
<div class="cell">~5%</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const active = ref('rest')
const styles = [
{
id: 'rpc',
icon: '📞',
name: 'RPC',
badge: '最传统',
desc: 'Remote Procedure Call,远程过程调用。像调用本地方法一样调用远程服务,面向过程,简单直接。超过 50% 的内部 API 采用这种风格。',
example: `GET /getUserInfo?id=123
POST /createUser
POST /deleteOrder
GET /queryUserList`,
features: [
'URL 命名往往是动词',
'HTTP 方法基本只用 GET/POST',
'设计简单,几乎无约束',
'需要详细文档说明'
],
scenarios: '内部 API、性能敏感场景、难以抽象为资源的业务',
official: '无官方规范(概念性风格)'
},
{
id: 'rest',
icon: '🌐',
name: 'REST',
badge: '最常用',
desc: 'Representational State Transfer,表述性状态转移。由 Roy Fielding 于 2000 年在其博士论文中提出。面向资源,用 URL 标识资源,用 HTTP 方法操作资源。',
example: `GET /users # 获取用户列表
GET /users/123 # 获取单个用户
POST /users # 创建用户
PUT /users/123 # 全量更新
PATCH /users/123 # 部分更新
DELETE /users/123 # 删除用户`,
features: [
'URL 是名词,不是动词',
'使用 HTTP 方法表达操作',
'无状态,请求包含所有信息',
'可缓存,支持分层系统'
],
scenarios: '公开 API、CRUD 操作、资源边界清晰的业务',
official: 'https://restfulapi.net/'
},
{
id: 'graphql',
icon: '📊',
name: 'GraphQL',
badge: '最灵活',
desc: '由 Facebook 于 2015 年开源。一种查询语言,客户端可以精确指定需要的数据字段,避免过度获取或获取不足。',
example: `query {
user(id: "123") {
name
email
orders {
id
total
}
}
}`,
features: [
'单一端点(/graphql',
'客户端决定返回字段',
'Schema 即文档',
'一次请求获取多资源'
],
scenarios: '客户端需求多变、数据关系复杂、移动端 App',
official: 'https://graphql.org/'
},
{
id: 'grpc',
icon: '⚡',
name: 'gRPC',
badge: '最高效',
desc: '由 Google 于 2016 年开源。高性能 RPC 框架,使用 Protocol Buffers 序列化,基于 HTTP/2,支持双向流通信。',
example: `service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc CreateUser(CreateUserRequest) returns (User);
}
message User {
string id = 1;
string name = 2;
}`,
features: [
'二进制传输,性能极高',
'强类型,代码自动生成',
'基于 HTTP/2,双向流',
'浏览器支持差'
],
scenarios: '微服务内部通信、高性能场景、强类型需求',
official: 'https://grpc.io/'
}
]
const currentStyle = computed(() => {
return styles.find((s) => s.id === active.value) || styles[1]
})
</script>
<style scoped>
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.header {
padding: 14px 20px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 10px;
}
.icon {
font-size: 20px;
}
.title {
font-weight: 600;
font-size: 15px;
}
.tabs {
display: flex;
gap: 6px;
padding: 12px 16px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
overflow-x: auto;
}
.tab {
padding: 8px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-soft);
font-size: 13px;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
}
.tab:hover {
border-color: var(--vp-c-brand);
}
.tab.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.content {
padding: 20px;
}
.style-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.style-header h4 {
margin: 0;
font-size: 18px;
}
.badge {
padding: 3px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
background: color-mix(in srgb, var(--vp-c-brand) 15%, transparent);
color: var(--vp-c-brand);
}
.desc {
font-size: 14px;
color: var(--vp-c-text-2);
line-height: 1.6;
margin: 0 0 16px 0;
}
.example-section {
margin-bottom: 16px;
}
.example-label {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.code-block {
background: #1e293b;
color: #e2e8f0;
padding: 14px;
border-radius: 8px;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 12px;
line-height: 1.6;
overflow-x: auto;
margin: 0;
}
.features {
margin-bottom: 16px;
}
.features-title {
font-size: 13px;
font-weight: 600;
margin-bottom: 10px;
}
.features-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
@media (max-width: 640px) {
.features-grid {
grid-template-columns: 1fr;
}
}
.feature-item {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
color: var(--vp-c-text-2);
}
.check {
color: var(--vp-c-brand);
font-weight: bold;
}
.meta {
padding-top: 14px;
border-top: 1px solid var(--vp-c-divider);
}
.meta-row {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 8px;
font-size: 13px;
}
.meta-label {
color: var(--vp-c-text-3);
min-width: 70px;
flex-shrink: 0;
}
.meta-value {
color: var(--vp-c-text-2);
}
.meta-link {
color: var(--vp-c-brand);
text-decoration: none;
word-break: break-all;
}
.meta-link:hover {
text-decoration: underline;
}
.compare-section {
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
padding: 16px 20px;
}
.compare-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
}
.compare-table {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
}
.compare-row {
display: grid;
grid-template-columns: 1fr repeat(4, 1fr);
}
.compare-row:nth-child(odd) {
background: var(--vp-c-bg-soft);
}
.compare-row:nth-child(even) {
background: var(--vp-c-bg);
}
.compare-row.head {
background: var(--vp-c-bg-alt);
}
.cell {
padding: 10px 8px;
font-size: 12px;
color: var(--vp-c-text-2);
text-align: center;
border-right: 1px solid var(--vp-c-divider);
}
.cell:last-child {
border-right: none;
}
.head .cell {
font-weight: 600;
color: var(--vp-c-text-1);
}
.cell:first-child {
text-align: left;
font-weight: 500;
color: var(--vp-c-text-1);
padding-left: 12px;
}
.cell.highlight {
background: color-mix(in srgb, var(--vp-c-brand) 10%, transparent);
color: var(--vp-c-brand);
font-weight: 600;
}
.cell.low {
color: #22c55e;
}
.cell.high {
color: #f59e0b;
}
.cell.best {
color: #22c55e;
font-weight: 600;
}
@media (max-width: 640px) {
.compare-row {
grid-template-columns: 70px repeat(4, 1fr);
}
.cell {
padding: 8px 4px;
font-size: 11px;
}
}
</style>
@@ -0,0 +1,533 @@
<template>
<div class="demo">
<div class="header">
<span class="icon">📦</span>
<span class="title">data 字段设计规范</span>
</div>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab', { active: active === tab.id }]"
@click="active = tab.id"
>
{{ tab.icon }} {{ tab.name }}
</button>
</div>
<div class="content">
<div v-if="active === 'structure'" class="section">
<h4>单对象 vs 列表</h4>
<div class="compare-row">
<div class="compare-col">
<div class="compare-title">单对象</div>
<pre class="code-sm">{
"code": 0,
"data": {
"id": 123,
"name": "张三"
}
}</pre>
</div>
<div class="compare-col">
<div class="compare-title">列表</div>
<pre class="code-sm">{
"code": 0,
"data": {
"items": [...],
"pagination": {
"page": 1,
"total": 100
}
}
}</pre>
</div>
</div>
<div class="note">列表数据包裹在 items 数组中分页信息放在 pagination 对象</div>
</div>
<div v-if="active === 'naming'" class="section">
<h4>字段命名规范</h4>
<div class="rule-list">
<div
v-for="rule in namingRules"
:key="rule.name"
class="rule-item"
>
<div class="rule-header">
<span class="rule-icon">{{ rule.icon }}</span>
<span class="rule-name">{{ rule.name }}</span>
</div>
<div class="rule-examples">
<code class="good">{{ rule.good }}</code>
<span
v-if="rule.bad"
class="vs"
>vs</span>
<code
v-if="rule.bad"
class="bad"
>{{ rule.bad }}</code>
</div>
<div class="rule-desc">{{ rule.desc }}</div>
</div>
</div>
</div>
<div v-if="active === 'datetime'" class="section">
<h4>时间格式设计</h4>
<div class="time-example">
<pre class="code-block">{
"created_at": "2024-01-15T09:30:00.000Z",
"updated_at": "2024-01-15T10:00:00.000Z",
"expired_at": "2025-01-15T00:00:00.000Z"
}</pre>
</div>
<div class="time-rules">
<div class="time-rule">
<span class="rule-label">格式</span>
<span class="rule-value">ISO 8601</span>
</div>
<div class="time-rule">
<span class="rule-label">时区</span>
<span class="rule-value">UTCZ 后缀或明确偏移量</span>
</div>
<div class="time-rule">
<span class="rule-label">精度</span>
<span class="rule-value">毫秒 .000Z</span>
</div>
<div class="time-rule">
<span class="rule-label">命名</span>
<span class="rule-value">xxx_at 表示时间点xxx_duration 表示时长</span>
</div>
</div>
</div>
<div v-if="active === 'null'" class="section">
<h4>空值处理</h4>
<div class="compare-row">
<div class="compare-col good-col">
<div class="compare-title"> 推荐</div>
<pre class="code-sm">{
"name": "张三",
"nickname": null,
"avatar": null
}</pre>
<div class="compare-desc">字段存在但无值时返回 null</div>
</div>
<div class="compare-col bad-col">
<div class="compare-title"> 不推荐</div>
<pre class="code-sm">{
"name": "张三"
}</pre>
<div class="compare-desc">省略字段前端需判断是否存在</div>
</div>
</div>
<div class="null-tips">
<div class="tip-item">空数组返回 <code>[]</code></div>
<div class="tip-item">空对象返回 <code>{}</code></div>
<div class="tip-item">前端可统一处理无需判断字段是否存在</div>
</div>
</div>
<div v-if="active === 'relation'" class="section">
<h4>关联数据设计</h4>
<div class="relation-tabs">
<button
v-for="r in relations"
:key="r.id"
:class="['r-tab', { active: rId === r.id }]"
@click="rId = r.id"
>
{{ r.name }}
</button>
</div>
<div class="relation-content">
<div class="relation-desc">{{ currentRelation.desc }}</div>
<pre class="code-block"><code>{{ currentRelation.code }}</code></pre>
</div>
</div>
</div>
<div class="tips">
<span class="tips-icon">💡</span>
<span class="tips-text">参考 ISO 8601 时间标准字段命名保持 snake_case 风格</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const active = ref('structure')
const rId = ref('embed')
const tabs = [
{ id: 'structure', icon: '📐', name: '结构设计' },
{ id: 'naming', icon: '📝', name: '命名规范' },
{ id: 'datetime', icon: '🕐', name: '时间格式' },
{ id: 'null', icon: '∅', name: '空值处理' },
{ id: 'relation', icon: '🔗', name: '关联数据' }
]
const namingRules = [
{ icon: '🔡', name: '使用 snake_case', good: 'created_at', bad: 'createdAt', desc: 'JSON 字段名统一用下划线' },
{ icon: '📖', name: '避免缩写', good: 'user_id', bad: 'uid', desc: '保持可读性' },
{ icon: '✅', name: '布尔值加前缀', good: 'is_active, has_permission', bad: 'active, permission', desc: '一眼识别布尔类型' },
{ icon: '📅', name: '时间带后缀', good: 'created_at, expired_at', bad: 'created, expired', desc: '明确是时间字段' },
{ icon: '🔢', name: '数量带后缀', good: 'total_count, page_size', bad: 'total, size', desc: '明确是数值类型' }
]
const relations = [
{
id: 'embed',
name: '内嵌',
desc: '适合数据量小、频繁访问的关联数据',
code: `{
"id": 123,
"name": "张三",
"department": {
"id": 1,
"name": "技术部"
}
}`
},
{
id: 'foreign',
name: '外键',
desc: '适合数据量大、按需加载的关联数据',
code: `{
"id": 123,
"name": "张三",
"department_id": 1
}`
},
{
id: 'expand',
name: 'expand 参数',
desc: 'Stripe 风格,客户端按需展开',
code: `// GET /users/123?expand=department
{
"id": 123,
"name": "张三",
"department": {
"id": 1,
"name": "技术部"
}
}`
}
]
const currentRelation = computed(() => {
return relations.find(r => r.id === rId.value) || relations[0]
})
</script>
<style scoped>
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.header {
padding: 14px 20px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 10px;
}
.icon {
font-size: 20px;
}
.title {
font-weight: 600;
font-size: 15px;
}
.tabs {
display: flex;
gap: 4px;
padding: 10px 12px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
overflow-x: auto;
}
.tab {
padding: 6px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-soft);
font-size: 12px;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
}
.tab:hover {
border-color: var(--vp-c-brand);
}
.tab.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.content {
padding: 16px;
}
.section h4 {
margin: 0 0 12px 0;
font-size: 14px;
}
.compare-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
@media (max-width: 640px) {
.compare-row {
grid-template-columns: 1fr;
}
}
.compare-col {
padding: 12px;
border-radius: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
}
.compare-col.good-col {
border-color: color-mix(in srgb, #22c55e 30%, transparent);
background: color-mix(in srgb, #22c55e 5%, var(--vp-c-bg));
}
.compare-col.bad-col {
border-color: color-mix(in srgb, #ef4444 30%, transparent);
background: color-mix(in srgb, #ef4444 5%, var(--vp-c-bg));
}
.compare-title {
font-size: 12px;
font-weight: 600;
margin-bottom: 8px;
}
.compare-desc {
font-size: 11px;
color: var(--vp-c-text-3);
margin-top: 8px;
}
.code-sm {
background: #1e293b;
color: #e2e8f0;
padding: 10px;
border-radius: 6px;
font-family: 'Menlo', monospace;
font-size: 11px;
line-height: 1.5;
overflow-x: auto;
margin: 0;
}
.code-block {
background: #1e293b;
color: #e2e8f0;
padding: 12px;
border-radius: 6px;
font-family: 'Menlo', monospace;
font-size: 11px;
line-height: 1.5;
overflow-x: auto;
margin: 0;
}
.note {
font-size: 12px;
color: var(--vp-c-text-2);
margin-top: 12px;
padding: 8px 12px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.rule-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.rule-item {
padding: 12px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
}
.rule-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.rule-icon {
font-size: 16px;
}
.rule-name {
font-size: 13px;
font-weight: 600;
}
.rule-examples {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.rule-examples code {
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
}
.rule-examples .good {
background: color-mix(in srgb, #22c55e 15%, transparent);
color: #16a34a;
}
.rule-examples .bad {
background: color-mix(in srgb, #ef4444 15%, transparent);
color: #dc2626;
text-decoration: line-through;
}
.rule-examples .vs {
font-size: 10px;
color: var(--vp-c-text-3);
}
.rule-desc {
font-size: 11px;
color: var(--vp-c-text-3);
}
.time-example {
margin-bottom: 12px;
}
.time-rules {
display: flex;
flex-direction: column;
gap: 6px;
}
.time-rule {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.rule-label {
font-size: 12px;
font-weight: 600;
color: var(--vp-c-text-2);
min-width: 40px;
}
.rule-value {
font-size: 12px;
color: var(--vp-c-text-1);
}
.null-tips {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.tip-item {
font-size: 12px;
color: var(--vp-c-text-2);
padding: 6px 10px;
background: var(--vp-c-bg);
border-radius: 4px;
}
.tip-item code {
background: var(--vp-c-bg-soft);
padding: 1px 5px;
border-radius: 3px;
font-size: 11px;
}
.relation-tabs {
display: flex;
gap: 4px;
margin-bottom: 12px;
}
.r-tab {
padding: 6px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.r-tab:hover {
border-color: var(--vp-c-brand);
}
.r-tab.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.relation-desc {
font-size: 12px;
color: var(--vp-c-text-2);
margin-bottom: 10px;
}
.tips {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
}
.tips-icon {
font-size: 14px;
}
.tips-text {
font-size: 12px;
color: var(--vp-c-text-2);
}
</style>
@@ -0,0 +1,575 @@
<template>
<div class="demo">
<div class="header">
<span class="icon"></span>
<span class="title">错误响应设计进阶</span>
</div>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab', { active: active === tab.id }]"
@click="active = tab.id"
>
{{ tab.icon }} {{ tab.name }}
</button>
</div>
<div class="content">
<div v-if="active === 'validate'" class="section">
<h4>参数校验错误</h4>
<pre class="code-block">{
"code": 10001,
"message": "参数校验失败",
"data": {
"errors": [
{
"field": "email",
"message": "邮箱格式不正确",
"value": "invalid-email"
},
{
"field": "password",
"message": "密码长度至少 8 位",
"value": "123"
}
]
}
}</pre>
<div class="field-tips">
<div class="tip-row">
<code>field</code>
<span>出错字段名前端可定位表单</span>
</div>
<div class="tip-row">
<code>message</code>
<span>用户友好的错误描述</span>
</div>
<div class="tip-row">
<code>value</code>
<span>客户端提交的值可选</span>
</div>
</div>
</div>
<div v-if="active === 'business'" class="section">
<h4>业务错误</h4>
<pre class="code-block">{
"code": 20001,
"message": "余额不足",
"data": {
"current_balance": 50.00,
"required_amount": 99.00,
"shortfall": 49.00,
"suggestion": "请充值后重试"
}
}</pre>
<div class="business-tips">
<div class="b-tip"> 返回当前状态数据便于前端展示</div>
<div class="b-tip"> 提供 suggestion 给出解决建议</div>
<div class="b-tip"> 数据结构化前端可灵活展示</div>
</div>
</div>
<div v-if="active === 'layers'" class="section">
<h4>错误码分层设计</h4>
<div class="layer-list">
<div
v-for="layer in layers"
:key="layer.range"
class="layer-item"
>
<div class="layer-range">{{ layer.range }}</div>
<div class="layer-info">
<div class="layer-name">{{ layer.name }}</div>
<div class="layer-example">示例{{ layer.example }}</div>
</div>
<div class="layer-desc">{{ layer.desc }}</div>
</div>
</div>
<div class="layer-note">错误码从外到内系统 服务 业务 认证 参数</div>
</div>
<div v-if="active === 'http'" class="section">
<h4>HTTP 状态码 vs 业务状态码</h4>
<div class="http-compare">
<div class="http-col">
<div class="http-title">HTTP 状态码</div>
<div class="http-desc">传输层状态</div>
<div class="http-codes">
<div class="http-code">
<span class="code-num">2xx</span>
<span>请求成功</span>
</div>
<div class="http-code">
<span class="code-num">4xx</span>
<span>客户端错误</span>
</div>
<div class="http-code">
<span class="code-num">5xx</span>
<span>服务端错误</span>
</div>
</div>
</div>
<div class="http-arrow"></div>
<div class="http-col">
<div class="http-title">业务状态码</div>
<div class="http-desc">业务层状态</div>
<div class="http-codes">
<div class="http-code">
<span class="code-num">0</span>
<span>业务成功</span>
</div>
<div class="http-code">
<span class="code-num">1xxxx</span>
<span>参数错误</span>
</div>
<div class="http-code">
<span class="code-num">2xxxx</span>
<span>业务错误</span>
</div>
</div>
</div>
</div>
<div class="http-note">HTTP 200 + 业务错误码 是业界主流做法</div>
</div>
<div v-if="active === 'examples'" class="section">
<h4>常见错误码示例</h4>
<div class="ex-tabs">
<button
v-for="ex in examples"
:key="ex.id"
:class="['ex-tab', { active: exId === ex.id }]"
@click="exId = ex.id"
>
{{ ex.name }}
</button>
</div>
<div class="ex-content">
<div class="ex-list">
<div
v-for="item in currentExample.items"
:key="item.code"
class="ex-row"
>
<code class="ex-code">{{ item.code }}</code>
<span class="ex-msg">{{ item.message }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="tips">
<span class="tips-icon">💡</span>
<span class="tips-text">错误信息要"机器可读 + 人类友好"便于前端统一处理</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const active = ref('validate')
const exId = ref('param')
const tabs = [
{ id: 'validate', icon: '🔍', name: '参数校验' },
{ id: 'business', icon: '💼', name: '业务错误' },
{ id: 'layers', icon: '📊', name: '分层设计' },
{ id: 'http', icon: '🌐', name: 'HTTP对比' },
{ id: 'examples', icon: '📋', name: '常见示例' }
]
const layers = [
{ range: '50001-59999', name: '系统层', example: '50001 数据库异常', desc: '基础设施问题' },
{ range: '40001-49999', name: '服务层', example: '40001 第三方服务超时', desc: '外部依赖问题' },
{ range: '30001-39999', name: '认证层', example: '30001 未登录', desc: '身份权限问题' },
{ range: '20001-29999', name: '业务层', example: '20001 余额不足', desc: '业务规则校验' },
{ range: '10001-19999', name: '参数层', example: '10001 参数缺失', desc: '客户端输入问题' }
]
const examples = [
{
id: 'param',
name: '参数层',
items: [
{ code: 10001, message: '缺少必填参数' },
{ code: 10002, message: '参数格式错误' },
{ code: 10003, message: '参数长度超限' },
{ code: 10004, message: '参数值非法' }
]
},
{
id: 'auth',
name: '认证层',
items: [
{ code: 30001, message: '未登录' },
{ code: 30002, message: '登录已过期' },
{ code: 30003, message: '无权限访问' },
{ code: 30004, message: '账号已被禁用' }
]
},
{
id: 'biz',
name: '业务层',
items: [
{ code: 20001, message: '余额不足' },
{ code: 20002, message: '商品已下架' },
{ code: 20003, message: '订单已取消' },
{ code: 20004, message: '库存不足' }
]
},
{
id: 'sys',
name: '系统层',
items: [
{ code: 50001, message: '数据库异常' },
{ code: 50002, message: '缓存服务异常' },
{ code: 50003, message: '系统繁忙,请稍后重试' }
]
}
]
const currentExample = computed(() => {
return examples.find(e => e.id === exId.value) || examples[0]
})
</script>
<style scoped>
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.header {
padding: 14px 20px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 10px;
}
.icon {
font-size: 20px;
}
.title {
font-weight: 600;
font-size: 15px;
}
.tabs {
display: flex;
gap: 4px;
padding: 10px 12px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
overflow-x: auto;
}
.tab {
padding: 6px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-soft);
font-size: 12px;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
}
.tab:hover {
border-color: var(--vp-c-brand);
}
.tab.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.content {
padding: 16px;
}
.section h4 {
margin: 0 0 12px 0;
font-size: 14px;
}
.code-block {
background: #1e293b;
color: #e2e8f0;
padding: 12px;
border-radius: 6px;
font-family: 'Menlo', monospace;
font-size: 11px;
line-height: 1.5;
overflow-x: auto;
margin: 0;
}
.field-tips {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.tip-row {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.tip-row code {
background: var(--vp-c-bg-soft);
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
color: var(--vp-c-brand);
min-width: 70px;
}
.tip-row span {
font-size: 12px;
color: var(--vp-c-text-2);
}
.business-tips {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.b-tip {
font-size: 12px;
color: var(--vp-c-text-2);
padding: 6px 10px;
background: var(--vp-c-bg);
border-radius: 4px;
}
.layer-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.layer-item {
display: grid;
grid-template-columns: 100px 1fr auto;
gap: 12px;
align-items: center;
padding: 10px 12px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
}
@media (max-width: 640px) {
.layer-item {
grid-template-columns: 1fr;
gap: 6px;
}
}
.layer-range {
font-family: monospace;
font-size: 12px;
font-weight: 600;
color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
padding: 4px 8px;
border-radius: 4px;
text-align: center;
}
.layer-name {
font-size: 13px;
font-weight: 600;
}
.layer-example {
font-size: 11px;
color: var(--vp-c-text-3);
}
.layer-desc {
font-size: 11px;
color: var(--vp-c-text-2);
background: color-mix(in srgb, var(--vp-c-brand) 10%, transparent);
padding: 4px 8px;
border-radius: 4px;
}
.layer-note {
margin-top: 12px;
font-size: 12px;
color: var(--vp-c-text-2);
padding: 8px 12px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.http-compare {
display: flex;
align-items: stretch;
gap: 12px;
}
@media (max-width: 640px) {
.http-compare {
flex-direction: column;
}
.http-arrow {
display: none;
}
}
.http-col {
flex: 1;
padding: 12px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
}
.http-title {
font-size: 13px;
font-weight: 600;
margin-bottom: 4px;
}
.http-desc {
font-size: 11px;
color: var(--vp-c-text-3);
margin-bottom: 10px;
}
.http-arrow {
display: flex;
align-items: center;
font-size: 20px;
color: var(--vp-c-text-3);
}
.http-codes {
display: flex;
flex-direction: column;
gap: 6px;
}
.http-code {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
}
.code-num {
font-family: monospace;
font-weight: 600;
color: var(--vp-c-brand);
min-width: 50px;
}
.http-note {
margin-top: 12px;
font-size: 12px;
color: var(--vp-c-text-2);
padding: 8px 12px;
background: color-mix(in srgb, #22c55e 10%, var(--vp-c-bg));
border-radius: 6px;
}
.ex-tabs {
display: flex;
gap: 4px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.ex-tab {
padding: 5px 10px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
font-size: 11px;
cursor: pointer;
transition: all 0.2s;
}
.ex-tab:hover {
border-color: var(--vp-c-brand);
}
.ex-tab.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.ex-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.ex-row {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: var(--vp-c-bg);
border-radius: 6px;
}
.ex-code {
font-family: monospace;
font-size: 12px;
font-weight: 600;
color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
padding: 2px 8px;
border-radius: 4px;
min-width: 50px;
text-align: center;
}
.ex-msg {
font-size: 12px;
color: var(--vp-c-text-2);
}
.tips {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
}
.tips-icon {
font-size: 14px;
}
.tips-text {
font-size: 12px;
color: var(--vp-c-text-2);
}
</style>
@@ -0,0 +1,603 @@
<template>
<div class="demo">
<div class="header">
<span class="icon">📋</span>
<span class="title">API 响应结构设计</span>
</div>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab', { active: active === tab.id }]"
@click="active = tab.id"
>
{{ tab.icon }} {{ tab.name }}
</button>
</div>
<div class="content">
<div v-if="active === 'why'" class="section">
<h4>为什么要统一响应格式</h4>
<div class="problem-box">
<div class="problem-title"> 问题不同接口返回格式不一致</div>
<pre class="code-sm">
// 接口 A
{ "data": { "user": {...} } }
// 接口 B
{ "result": { "user": {...} } }
// 接口 C
{ "user": {...} }</pre>
<div class="problem-desc">
前端需要针对每个接口单独处理代码冗余容易出错
</div>
</div>
<div class="solution-box">
<div class="solution-title"> 解决统一响应格式</div>
<pre class="code-sm">
{
"code": 0,
"message": "success",
"data": { ... },
"request_id": "req-xxx"
}</pre>
</div>
</div>
<div v-if="active === 'fields'" class="section">
<h4>响应字段说明</h4>
<div class="field-list">
<div v-for="field in fields" :key="field.name" class="field-item">
<div class="field-header">
<code class="field-name">{{ field.name }}</code>
<span class="field-type">{{ field.type }}</span>
<span v-if="field.required" class="field-required">必填</span>
</div>
<div class="field-desc">{{ field.desc }}</div>
</div>
</div>
</div>
<div v-if="active === 'codes'" class="section">
<h4>业务状态码设计</h4>
<div class="code-ranges">
<div class="range-item">
<span class="range-num">0</span>
<span class="range-label">成功</span>
</div>
<div class="range-item">
<span class="range-num">1xxxx</span>
<span class="range-label">客户端错误</span>
</div>
<div class="range-item">
<span class="range-num">2xxxx</span>
<span class="range-label">业务错误</span>
</div>
<div class="range-item">
<span class="range-num">3xxxx</span>
<span class="range-label">认证/权限错误</span>
</div>
<div class="range-item">
<span class="range-num">5xxxx</span>
<span class="range-label">系统错误</span>
</div>
</div>
<div class="code-examples">
<div v-for="code in codeExamples" :key="code.code" class="code-row">
<code class="code-value">{{ code.code }}</code>
<span class="code-msg">{{ code.message }}</span>
</div>
</div>
</div>
<div v-if="active === 'examples'" class="section">
<h4>不同场景响应示例</h4>
<div class="example-tabs">
<button
v-for="ex in examples"
:key="ex.id"
:class="['ex-tab', { active: exId === ex.id }]"
@click="exId = ex.id"
>
{{ ex.name }}
</button>
</div>
<div class="example-content">
<pre class="code-block"><code>{{ currentExample.code }}</code></pre>
<div class="example-note">{{ currentExample.note }}</div>
</div>
</div>
<div v-if="active === 'pagination'" class="section">
<h4>分页参数设计</h4>
<div class="pg-row">
<div class="pg-col">
<div class="pg-title">请求参数</div>
<div class="pg-params">
<div class="pg-item">
<code>page</code>
<span>页码 1 开始</span>
</div>
<div class="pg-item">
<code>page_size</code>
<span>每页数量默认 20</span>
</div>
<div class="pg-item">
<code>sort</code>
<span>排序 created_desc</span>
</div>
</div>
</div>
<div class="pg-col">
<div class="pg-title">响应格式</div>
<pre class="code-sm">
"pagination": {
"page": 1,
"page_size": 20,
"total": 156,
"total_pages": 8,
"has_next": true
}</pre>
</div>
</div>
</div>
</div>
<div class="tips">
<span class="tips-icon">💡</span>
<span class="tips-text">request_id 用于问题追踪建议使用 UUID v4 或雪花算法生成</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const active = ref('why')
const exId = ref('success')
const tabs = [
{ id: 'why', icon: '❓', name: '为什么统一' },
{ id: 'fields', icon: '📝', name: '字段说明' },
{ id: 'codes', icon: '🔢', name: '状态码' },
{ id: 'examples', icon: '📄', name: '示例' },
{ id: 'pagination', icon: '📑', name: '分页' }
]
const fields = [
{
name: 'code',
type: 'number',
required: true,
desc: '业务状态码,0 表示成功'
},
{ name: 'message', type: 'string', required: true, desc: '状态描述信息' },
{
name: 'data',
type: 'any',
required: false,
desc: '业务数据,失败时可为 null'
},
{
name: 'request_id',
type: 'string',
required: true,
desc: '请求唯一标识,用于追踪'
},
{
name: 'timestamp',
type: 'string',
required: false,
desc: '响应时间戳,ISO 8601 格式'
}
]
const codeExamples = [
{ code: 0, message: 'success - 成功' },
{ code: 10001, message: '参数错误:缺少必填字段' },
{ code: 10002, message: '资源不存在' },
{ code: 20001, message: '余额不足' },
{ code: 30001, message: '未登录' },
{ code: 50001, message: '系统繁忙,请稍后重试' }
]
const examples = [
{
id: 'success',
name: '成功-单对象',
code: `{
"code": 0,
"message": "success",
"data": {
"id": 123,
"name": "张三",
"email": "zhangsan@example.com"
},
"request_id": "req-abc123"
}`,
note: '成功响应:data 包含具体业务数据'
},
{
id: 'list',
name: '成功-列表',
code: `{
"code": 0,
"message": "success",
"data": {
"items": [
{ "id": 1, "name": "商品A" },
{ "id": 2, "name": "商品B" }
],
"pagination": {
"page": 1,
"page_size": 20,
"total": 156
}
},
"request_id": "req-def456"
}`,
note: '列表响应:items 数组 + pagination 分页信息'
},
{
id: 'error',
name: '业务错误',
code: `{
"code": 20001,
"message": "余额不足,当前余额 50.00 元",
"data": null,
"request_id": "req-ghi789"
}`,
note: '业务错误:code 非 0message 说明原因'
},
{
id: 'validate',
name: '参数校验',
code: `{
"code": 10001,
"message": "参数校验失败",
"data": {
"errors": [
{ "field": "email", "message": "邮箱格式不正确" },
{ "field": "password", "message": "密码长度至少 8 位" }
]
},
"request_id": "req-jkl012"
}`,
note: '参数错误:data.errors 列出所有错误字段'
}
]
const currentExample = computed(() => {
return examples.find((e) => e.id === exId.value) || examples[0]
})
</script>
<style scoped>
.demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
background: var(--vp-c-bg-soft);
margin: 24px 0;
overflow: hidden;
}
.header {
padding: 14px 20px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
gap: 10px;
}
.icon {
font-size: 20px;
}
.title {
font-weight: 600;
font-size: 15px;
}
.tabs {
display: flex;
gap: 4px;
padding: 10px 12px;
background: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
overflow-x: auto;
}
.tab {
padding: 6px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-soft);
font-size: 12px;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
}
.tab:hover {
border-color: var(--vp-c-brand);
}
.tab.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.content {
padding: 16px;
}
.section h4 {
margin: 0 0 12px 0;
font-size: 14px;
}
.problem-box,
.solution-box {
margin-bottom: 12px;
padding: 12px;
border-radius: 8px;
}
.problem-box {
background: color-mix(in srgb, #ef4444 8%, var(--vp-c-bg));
border: 1px solid color-mix(in srgb, #ef4444 20%, transparent);
}
.solution-box {
background: color-mix(in srgb, #22c55e 8%, var(--vp-c-bg));
border: 1px solid color-mix(in srgb, #22c55e 20%, transparent);
}
.problem-title,
.solution-title {
font-size: 13px;
font-weight: 600;
margin-bottom: 8px;
}
.problem-desc {
font-size: 12px;
color: var(--vp-c-text-3);
margin-top: 8px;
}
.code-sm {
background: #1e293b;
color: #e2e8f0;
padding: 10px;
border-radius: 6px;
font-family: 'Menlo', monospace;
font-size: 11px;
line-height: 1.5;
overflow-x: auto;
margin: 0;
}
.field-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.field-item {
padding: 10px 12px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
}
.field-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.field-name {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-brand);
}
.field-type {
font-size: 11px;
color: var(--vp-c-text-3);
background: var(--vp-c-bg-soft);
padding: 2px 6px;
border-radius: 4px;
}
.field-required {
font-size: 10px;
color: #f59e0b;
background: color-mix(in srgb, #f59e0b 15%, transparent);
padding: 1px 5px;
border-radius: 3px;
}
.field-desc {
font-size: 12px;
color: var(--vp-c-text-2);
}
.code-ranges {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.range-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
}
.range-num {
font-family: monospace;
font-size: 12px;
font-weight: 600;
color: var(--vp-c-brand);
}
.range-label {
font-size: 11px;
color: var(--vp-c-text-2);
}
.code-examples {
display: flex;
flex-direction: column;
gap: 4px;
}
.code-row {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
background: var(--vp-c-bg);
border-radius: 4px;
}
.code-value {
font-family: monospace;
font-size: 12px;
font-weight: 600;
color: var(--vp-c-brand);
min-width: 50px;
}
.code-msg {
font-size: 12px;
color: var(--vp-c-text-2);
}
.example-tabs {
display: flex;
gap: 4px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.ex-tab {
padding: 5px 10px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
font-size: 11px;
cursor: pointer;
transition: all 0.2s;
}
.ex-tab:hover {
border-color: var(--vp-c-brand);
}
.ex-tab.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.code-block {
background: #1e293b;
color: #e2e8f0;
padding: 12px;
border-radius: 6px;
font-family: 'Menlo', monospace;
font-size: 11px;
line-height: 1.5;
overflow-x: auto;
margin: 0;
}
.example-note {
font-size: 11px;
color: var(--vp-c-text-3);
margin-top: 8px;
padding-left: 4px;
}
.pg-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
@media (max-width: 640px) {
.pg-row {
grid-template-columns: 1fr;
}
}
.pg-col {
padding: 12px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
}
.pg-title {
font-size: 12px;
font-weight: 600;
margin-bottom: 10px;
}
.pg-params {
display: flex;
flex-direction: column;
gap: 6px;
}
.pg-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
}
.pg-item code {
background: var(--vp-c-bg-soft);
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
color: var(--vp-c-brand);
}
.pg-item span {
color: var(--vp-c-text-2);
}
.tips {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: var(--vp-c-bg);
border-top: 1px solid var(--vp-c-divider);
}
.tips-icon {
font-size: 14px;
}
.tips-text {
font-size: 12px;
color: var(--vp-c-text-2);
}
</style>
@@ -0,0 +1,417 @@
<template>
<div class="raf-root">
<div class="raf-layout">
<!-- Left: Client Side -->
<div class="raf-left">
<div class="raf-header">
<span class="raf-icon">💻</span>
<span class="raf-title">Client (Browser/App)</span>
</div>
<div class="raf-controls">
<div class="raf-scenarios">
<button
v-for="s in scenarios"
:key="s.id"
:class="['raf-chip', { active: currentScenario.id === s.id }]"
@click="selectScenario(s)"
:disabled="processing"
>
{{ s.label }}
</button>
</div>
</div>
<div class="raf-request-box">
<div class="raf-http-line">
<span :class="['raf-method', currentScenario.method]">{{ currentScenario.method }}</span>
<span class="raf-url">{{ currentScenario.url }}</span>
</div>
<div v-if="currentScenario.body" class="raf-code-block">
{{ JSON.stringify(currentScenario.body, null, 2) }}
</div>
<button
class="raf-send-btn"
@click="sendRequest"
:disabled="processing"
>
{{ processing ? 'Sending...' : 'Send Request' }}
</button>
</div>
<div class="raf-response-box" v-if="response">
<div class="raf-status-line">
<span class="raf-label">Response Status:</span>
<span :class="['raf-status-badge', getStatusColor(response.status)]">
{{ response.status }} {{ response.statusText }}
</span>
</div>
<div class="raf-code-block response-body">
{{ JSON.stringify(response.body, null, 2) }}
</div>
</div>
</div>
<!-- Right: Server Side -->
<div class="raf-right">
<div class="raf-header server-header">
<span class="raf-icon"></span>
<span class="raf-title">Server (API)</span>
</div>
<div class="raf-server-state">
<!-- Database View -->
<div class="raf-section">
<div class="raf-section-title">📦 Database (Users Resource)</div>
<div class="raf-db-view">
<transition-group name="list">
<div v-for="user in db" :key="user.id" class="raf-db-item">
<span class="raf-db-id">ID: {{ user.id }}</span>
<span class="raf-db-name">{{ user.name }}</span>
<span class="raf-db-role">({{ user.role }})</span>
</div>
</transition-group>
<div v-if="db.length === 0" class="raf-empty">No users found</div>
</div>
</div>
<!-- Logs -->
<div class="raf-section">
<div class="raf-section-title">📜 Server Logs</div>
<div class="raf-logs" ref="logsRef">
<div v-for="(log, i) in logs" :key="i" class="raf-log-line">
<span class="raf-log-time">[{{ log.time }}]</span>
<span :class="log.type">{{ log.msg }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, nextTick } from 'vue'
const processing = ref(false)
const response = ref(null)
const logs = ref([])
const logsRef = ref(null)
const db = ref([
{ id: 1, name: "Alice", role: "admin" },
{ id: 2, name: "Bob", role: "user" }
])
const scenarios = [
{ id: 'get-all', label: 'GET /users', method: 'GET', url: '/api/users', body: null },
{ id: 'get-one', label: 'GET /users/1', method: 'GET', url: '/api/users/1', body: null },
{ id: 'create', label: 'POST /users', method: 'POST', url: '/api/users', body: { name: "Charlie", role: "user" } },
{ id: 'not-found', label: 'GET /users/99', method: 'GET', url: '/api/users/99', body: null },
{ id: 'delete', label: 'DELETE /users/1', method: 'DELETE', url: '/api/users/1', body: null },
]
const currentScenario = ref(scenarios[0])
function selectScenario(s) {
currentScenario.value = s
response.value = null
}
function addLog(msg, type = 'info') {
const now = new Date()
const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`
logs.value.push({ time, msg, type })
nextTick(() => {
if (logsRef.value) logsRef.value.scrollTop = logsRef.value.scrollHeight
})
}
function getStatusColor(status) {
if (status >= 200 && status < 300) return 'status-success'
if (status >= 400 && status < 500) return 'status-error'
return 'status-neutral'
}
async function sendRequest() {
processing.value = true
response.value = null
addLog(`Received ${currentScenario.value.method} ${currentScenario.value.url}`, 'info')
await new Promise(r => setTimeout(r, 600)) // Simulate network latency
const { method, url, body } = currentScenario.value
// Router Logic Simulation
if (method === 'GET' && url === '/api/users') {
response.value = { status: 200, statusText: 'OK', body: db.value }
addLog('Matched route: GET /users -> listUsers()', 'success')
}
else if (method === 'GET' && url.match(/\/api\/users\/\d+/)) {
const id = parseInt(url.split('/').pop())
const user = db.value.find(u => u.id === id)
if (user) {
response.value = { status: 200, statusText: 'OK', body: user }
addLog(`Found user ${id}`, 'success')
} else {
response.value = { status: 404, statusText: 'Not Found', body: { error: "User not found" } }
addLog(`User ${id} not found in DB`, 'error')
}
}
else if (method === 'POST' && url === '/api/users') {
const newUser = { id: Math.max(0, ...db.value.map(u => u.id)) + 1, ...body }
db.value.push(newUser)
response.value = { status: 201, statusText: 'Created', body: newUser }
addLog(`Created user ${newUser.id}`, 'success')
}
else if (method === 'DELETE' && url.match(/\/api\/users\/\d+/)) {
const id = parseInt(url.split('/').pop())
const idx = db.value.findIndex(u => u.id === id)
if (idx !== -1) {
db.value.splice(idx, 1)
response.value = { status: 204, statusText: 'No Content', body: null }
addLog(`Deleted user ${id}`, 'success')
} else {
response.value = { status: 404, statusText: 'Not Found', body: { error: "User not found" } }
addLog(`User ${id} not found for deletion`, 'error')
}
}
processing.value = false
}
</script>
<style scoped>
.raf-root {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
background: var(--vp-c-bg-soft);
margin: 1rem 0;
font-family: var(--vp-font-family-mono);
font-size: 13px;
}
.raf-layout {
display: flex;
min-height: 400px;
}
.raf-left, .raf-right {
flex: 1;
padding: 1.2rem;
display: flex;
flex-direction: column;
}
.raf-left {
border-right: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
}
.raf-right {
background: var(--vp-c-bg-alt);
}
.raf-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 1rem;
font-weight: 600;
font-size: 1.1em;
color: var(--vp-c-text-1);
}
.raf-scenarios {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 1.5rem;
}
.raf-chip {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
color: var(--vp-c-text-2);
}
.raf-chip:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.raf-chip.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.raf-request-box {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 1rem;
background: var(--vp-c-bg-soft);
margin-bottom: 1rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.raf-http-line {
display: flex;
gap: 10px;
font-family: monospace;
margin-bottom: 8px;
align-items: center;
font-size: 1.1em;
}
.raf-method {
font-weight: bold;
}
.raf-method.GET { color: #61affe; }
.raf-method.POST { color: #49cc90; }
.raf-method.DELETE { color: #f93e3e; }
.raf-code-block {
background: var(--vp-c-bg);
padding: 10px;
border-radius: 4px;
font-size: 12px;
white-space: pre;
overflow-x: auto;
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
}
.raf-send-btn {
margin-top: 10px;
width: 100%;
padding: 10px;
background: var(--vp-c-brand);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: opacity 0.2s;
}
.raf-send-btn:hover {
opacity: 0.9;
}
.raf-send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.raf-response-box {
margin-top: auto;
border-top: 1px solid var(--vp-c-divider);
padding-top: 1rem;
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.raf-status-line {
margin-bottom: 8px;
display: flex;
align-items: center;
}
.raf-status-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
margin-left: 8px;
}
.status-success { background: #d1fae5; color: #065f46; }
.status-error { background: #fee2e2; color: #991b1b; }
.status-neutral { background: #f3f4f6; color: #374151; }
.raf-db-view {
display: flex;
flex-direction: column;
gap: 6px;
}
.raf-db-item {
display: flex;
gap: 10px;
padding: 8px 12px;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
font-size: 12px;
align-items: center;
}
.raf-db-id { color: var(--vp-c-text-3); font-family: monospace; }
.raf-db-name { font-weight: bold; }
.raf-db-role { color: var(--vp-c-brand); font-size: 0.9em; }
.raf-logs {
height: 180px;
overflow-y: auto;
background: #1e1e1e;
color: #d4d4d4;
padding: 12px;
border-radius: 6px;
font-family: monospace;
font-size: 11px;
line-height: 1.5;
}
.raf-log-line {
display: flex;
gap: 8px;
margin-bottom: 4px;
}
.raf-log-time { color: #6b7280; flex-shrink: 0; }
.info { color: #93c5fd; }
.success { color: #86efac; }
.error { color: #fca5a5; }
.raf-section-title {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: bold;
margin-bottom: 8px;
color: var(--vp-c-text-3);
margin-top: 1.5rem;
}
.raf-section:first-child .raf-section-title { margin-top: 0; }
.raf-empty {
color: var(--vp-c-text-3);
font-style: italic;
padding: 10px;
text-align: center;
}
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(20px);
}
@media (max-width: 768px) {
.raf-layout { flex-direction: column; }
.raf-left { border-right: none; border-bottom: 1px solid var(--vp-c-divider); }
}
</style>
@@ -14,30 +14,15 @@
>
<h5>传统部署</h5>
<div class="server-stack">
<div class="layer-item app">
应用 A
</div>
<div
v-if="showConflict"
class="layer-item conflict"
>
依赖冲突!
</div>
<div class="layer-item deps">
依赖库 v1.0
</div>
<div class="layer-item os">
操作系统
</div>
<div class="layer-item hardware">
物理服务器
</div>
<div class="layer-item app">应用 A</div>
<div v-if="showConflict" class="layer-item conflict">依赖冲突!</div>
<div class="layer-item deps">依赖库 v1.0</div>
<div class="layer-item os">操作系统</div>
<div class="layer-item hardware">物理服务器</div>
</div>
</div>
<div class="vs-divider">
VS
</div>
<div class="vs-divider">VS</div>
<div
class="layer docker"
@@ -48,31 +33,17 @@
<div class="docker-stack">
<div class="containers">
<div class="container-box">
<div class="container-app">
应用 A
</div>
<div class="container-deps">
依赖 v1.0
</div>
<div class="container-app">应用 A</div>
<div class="container-deps">依赖 v1.0</div>
</div>
<div class="container-box">
<div class="container-app">
应用 B
</div>
<div class="container-deps">
依赖 v2.0
</div>
<div class="container-app">应用 B</div>
<div class="container-deps">依赖 v2.0</div>
</div>
</div>
<div class="docker-engine">
Docker Engine
</div>
<div class="host-os">
宿主机操作系统
</div>
<div class="hardware">
物理服务器
</div>
<div class="docker-engine">Docker Engine</div>
<div class="host-os">宿主机操作系统</div>
<div class="hardware">物理服务器</div>
</div>
</div>
</div>
@@ -110,9 +81,17 @@ const showDocker = ref(false)
const showConflict = ref(false)
const benefits = [
{ icon: '📦', title: '环境一致性', desc: '开发、测试、生产环境完全一致,告别"在我机器上能跑"' },
{
icon: '📦',
title: '环境一致性',
desc: '开发、测试、生产环境完全一致,告别"在我机器上能跑"'
},
{ icon: '🚀', title: '快速部署', desc: '秒级启动,镜像分发,滚动更新无停机' },
{ icon: '📊', title: '资源隔离', desc: 'CPU/内存限制,互不干扰,一台机器跑多个应用' },
{
icon: '📊',
title: '资源隔离',
desc: 'CPU/内存限制,互不干扰,一台机器跑多个应用'
},
{ icon: '🔄', title: '版本管理', desc: '镜像版本化,随时回滚,灰度发布' }
]
</script>
@@ -219,8 +198,13 @@ const benefits = [
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.containers {
@@ -1,164 +1,82 @@
<template>
<div class="adder-demo">
<div class="demo-header">
<span class="icon">🧮</span>
<span class="title">加法器CPU 怎么做加法</span>
<span class="subtitle">从手算竖式理解"逐位计算"的原理</span>
<div class="demo-label">二进制加法器 输入 015 的两个数观察逐位计算过程</div>
<div class="control-row">
<label class="input-group">
<span class="input-label">A</span>
<input v-model.number="inputA" type="number" min="0" max="15" class="num-input" />
</label>
<span class="op-sign">+</span>
<label class="input-group">
<span class="input-label">B</span>
<input v-model.number="inputB" type="number" min="0" max="15" class="num-input" />
</label>
<span class="op-sign">=</span>
<span class="result-num">{{ resultDec }}</span>
</div>
<div class="intro-section">
<div class="intro-title">🎯 先看十进制竖式理解"逐位计算"</div>
<div class="decimal-demo">
<div class="decimal-column">
<div class="decimal-row label-row">被加数</div>
<div class="decimal-row num-row">
<span class="d-digit">{{ decimalA }}</span>
</div>
</div>
<div class="decimal-column op-col">
<div class="decimal-row label-row">+</div>
<div class="decimal-row num-row">
<span class="d-digit">{{ decimalB }}</span>
</div>
</div>
<div class="decimal-column">
<div class="decimal-row label-row">结果</div>
<div class="decimal-row num-row result">
<span class="d-digit">{{ decimalA + decimalB }}</span>
</div>
</div>
<div class="binary-display">
<div class="binary-row">
<span class="binary-label">A</span>
<span class="binary-bits">
<span v-for="(b, i) in bitsA" :key="'a'+i" class="bit" :class="{ hl: activeBit === (3 - i) }">{{ b }}</span>
</span>
<span class="binary-dec">= {{ clampedA }}</span>
</div>
<div class="intro-hint">
<span class="icon">💡</span>
<span>手算时我们从<strong>个位往高位</strong>一位一位算<strong>逢十进一</strong>CPU 做加法也一样只是它只认识 0 1所以要<strong>逢二进一</strong></span>
<div class="binary-row">
<span class="binary-label">B</span>
<span class="binary-bits">
<span v-for="(b, i) in bitsB" :key="'b'+i" class="bit" :class="{ hl: activeBit === (3 - i) }">{{ b }}</span>
</span>
<span class="binary-dec">= {{ clampedB }}</span>
</div>
<div class="binary-row sum-row">
<span class="binary-label">结果</span>
<span class="binary-bits">
<span v-for="(b, i) in bitsSum" :key="'s'+i" class="bit" :class="{ hl: activeBit === (3 - i) }">{{ b }}</span>
</span>
<span class="binary-dec">= {{ fourBitResult }}</span>
</div>
<div class="bit-labels">
<span v-for="i in 4" :key="i" class="bit-label">{{ 4 - i }}</span>
</div>
</div>
<div class="concept-section">
<div class="concept-title">📚 核心概念</div>
<div class="concepts-grid">
<div class="concept-card half-adder">
<div class="concept-name">半加器</div>
<div class="concept-simple">只算 A + B</div>
<div class="concept-detail">
<p>最右边一位用因为<strong>没有进位进来</strong></p>
<p class="formula">输入AB 输出(S)进位(C)</p>
</div>
</div>
<div class="concept-card full-adder">
<div class="concept-name">全加器</div>
<div class="concept-simple"> A + B + 进位</div>
<div class="concept-detail">
<p>其他位用因为<strong>要加上一位的进位</strong></p>
<p class="formula">输入ABCin 输出(S)进位(Cout)</p>
</div>
</div>
</div>
</div>
<div class="demo-section">
<div class="demo-title">🎮 动手试试二进制加法</div>
<div class="control-row">
<label class="input-group">
<span class="input-label">A被加数</span>
<input v-model.number="inputA" type="number" min="0" max="15" class="num-input" />
</label>
<span class="op-sign">+</span>
<label class="input-group">
<span class="input-label">B加数</span>
<input v-model.number="inputB" type="number" min="0" max="15" class="num-input" />
</label>
<span class="op-sign">=</span>
<span class="result-num">{{ resultDec }}</span>
</div>
<div class="binary-display">
<div class="binary-row">
<span class="binary-label">A</span>
<span class="binary-bits">
<span v-for="(b, i) in bitsA" :key="'a'+i" class="bit" :class="{ highlight: activeBit === (3 - i) }">{{ b }}</span>
<div class="stages-row">
<div
v-for="(stage, idx) in stages" :key="idx"
class="stage-card"
:class="{ active: activeBit === stage.bitPos }"
@mouseenter="activeBit = stage.bitPos"
@mouseleave="activeBit = null"
>
<div class="stage-head">
<span class="stage-pos">{{ stage.bitPos }}</span>
<span class="stage-type" :class="stage.carryIn !== null ? 'full' : 'half'">
{{ stage.carryIn !== null ? '全加器' : '半加器' }}
</span>
<span class="binary-dec">= {{ inputA }}</span>
</div>
<div class="binary-row">
<span class="binary-label">B</span>
<span class="binary-bits">
<span v-for="(b, i) in bitsB" :key="'b'+i" class="bit" :class="{ highlight: activeBit === (3 - i) }">{{ b }}</span>
</span>
<span class="binary-dec">= {{ inputB }}</span>
<div class="stage-io">
<span class="io-item"><span class="io-tag a">A</span>{{ stage.a }}</span>
<span class="io-item"><span class="io-tag b">B</span>{{ stage.b }}</span>
<span v-if="stage.carryIn !== null" class="io-item"><span class="io-tag cin">Cin</span>{{ stage.carryIn }}</span>
</div>
<div class="binary-row result-row">
<span class="binary-label">结果</span>
<span class="binary-bits">
<span v-for="(b, i) in bitsSum" :key="'s'+i" class="bit" :class="{ highlight: activeBit === (3 - i) }">{{ b }}</span>
</span>
<span class="binary-dec">= {{ fourBitResult }}</span>
</div>
<div class="bit-labels">
<span v-for="i in 4" :key="i" class="bit-label">{{ 4 - i }}</span>
</div>
</div>
<div class="stages-row">
<div
v-for="(stage, idx) in stages"
:key="idx"
class="stage-card"
:class="{ active: activeBit === stage.bitPos }"
@mouseenter="activeBit = stage.bitPos"
@mouseleave="activeBit = null"
>
<div class="stage-header">
<span class="stage-pos">{{ stage.bitPos }}</span>
<span class="stage-type" :class="stage.carryIn !== null ? 'full' : 'half'">
{{ stage.carryIn !== null ? '全加器' : '半加器' }}
</span>
</div>
<div class="stage-io">
<div class="io-line">
<span class="io-tag a">A</span>
<span class="io-val">{{ stage.a }}</span>
</div>
<div class="io-line">
<span class="io-tag b">B</span>
<span class="io-val">{{ stage.b }}</span>
</div>
<div v-if="stage.carryIn !== null" class="io-line">
<span class="io-tag cin">Cin</span>
<span class="io-val">{{ stage.carryIn }}</span>
</div>
</div>
<div class="stage-divider"></div>
<div class="stage-io">
<div class="io-line">
<span class="io-tag s">S</span>
<span class="io-val sum">{{ stage.sum }}</span>
</div>
<div class="io-line">
<span class="io-tag cout">Cout</span>
<span class="io-val">{{ stage.carryOut }}</span>
</div>
</div>
<div v-if="idx < 3" class="carry-arrow" :class="{ hasCarry: stage.carryOut }">
{{ stage.carryOut ? ' 进位' : '' }}
</div>
<div class="stage-divider"></div>
<div class="stage-io">
<span class="io-item"><span class="io-tag s">S</span><strong>{{ stage.sum }}</strong></span>
<span class="io-item"><span class="io-tag cout">C</span>{{ stage.carryOut }}</span>
</div>
</div>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>核心思想</strong>每位加法器接收 AB 和上一位的进位输出本位的和与传给下一位的进位就像手算竖式"逢二进一"只是用电路自动完成
</div>
<div class="demo-caption">鼠标悬停某一位查看该位加法器的输入 / 输出 · 就像手算竖式"逢二进一"</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const decimalA = 35
const decimalB = 47
const inputA = ref(3)
const inputB = ref(2)
const activeBit = ref(null)
@@ -191,14 +109,7 @@ const stages = computed(() => {
sum = (a ^ b) ^ carryIn
carryOut = (a & b) | (carryIn & (a ^ b))
}
result.push({
bitPos: i,
a,
b,
carryIn: carryIn === null ? null : carryIn,
sum,
carryOut
})
result.push({ bitPos: i, a, b, carryIn: carryIn === null ? null : carryIn, sum, carryOut })
carryIn = carryOut
}
return result
@@ -215,7 +126,7 @@ const fourBitResult = computed(() =>
const overflow = computed(() => clampedA.value + clampedB.value > 15)
const resultDec = computed(() =>
overflow.value ? `${fourBitResult.value}4位溢出)` : String(fourBitResult.value)
overflow.value ? `${fourBitResult.value}(溢出)` : String(fourBitResult.value)
)
</script>
@@ -224,164 +135,25 @@ const resultDec = computed(() =>
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .icon { font-size: 1.25rem; }
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.25rem; }
.intro-section {
background: var(--vp-c-bg-alt);
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.75rem;
}
.intro-title {
.demo-label {
font-size: 0.78rem;
font-weight: bold;
font-size: 0.85rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.decimal-demo {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.decimal-column {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.decimal-column.op-col {
min-width: 2rem;
text-align: center;
}
.decimal-row {
font-size: 0.85rem;
}
.decimal-row.label-row {
color: var(--vp-c-text-3);
font-size: 0.75rem;
}
.decimal-row.num-row {
font-family: monospace;
font-size: 1.1rem;
font-weight: bold;
}
.decimal-row.num-row.result {
color: var(--vp-c-brand-1);
}
.d-digit {
display: inline-block;
min-width: 1.5rem;
text-align: center;
}
.intro-hint {
display: flex;
align-items: flex-start;
gap: 0.35rem;
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.intro-hint .icon {
flex-shrink: 0;
}
.concept-section {
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.concept-title {
font-weight: bold;
font-size: 0.85rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
.concepts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.concept-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.6rem;
}
.concept-name {
font-weight: bold;
font-size: 0.9rem;
margin-bottom: 0.15rem;
}
.concept-simple {
font-size: 0.8rem;
color: var(--vp-c-brand-1);
font-weight: 600;
margin-bottom: 0.25rem;
}
.concept-detail {
font-size: 0.75rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.concept-detail p {
margin: 0;
}
.concept-detail .formula {
margin-top: 0.2rem;
font-family: monospace;
color: var(--vp-c-text-3);
}
.half-adder .concept-name { color: var(--vp-c-brand-1); }
.full-adder .concept-name { color: #8b5cf6; }
.demo-section {
margin-bottom: 0.5rem;
}
.demo-title {
font-weight: bold;
font-size: 0.85rem;
margin-bottom: 0.5rem;
color: var(--vp-c-text-1);
}
/* ── controls ── */
.control-row {
display: flex;
align-items: center;
gap: 0.4rem;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 0.5rem;
margin-bottom: 0.6rem;
}
.input-group {
@@ -391,67 +163,70 @@ const resultDec = computed(() =>
}
.input-label {
font-size: 0.8rem;
font-size: 0.82rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.num-input {
width: 3rem;
padding: 0.2rem 0.35rem;
width: 3.2rem;
padding: 0.25rem 0.4rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
font-size: 0.85rem;
font-size: 0.9rem;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
}
.op-sign {
font-weight: bold;
color: var(--vp-c-text-2);
color: var(--vp-c-text-3);
}
.result-num {
font-weight: bold;
color: var(--vp-c-brand-1);
font-size: 0.95rem;
font-size: 1rem;
}
/* ── binary ── */
.binary-display {
background: var(--vp-c-bg-alt);
border-radius: 6px;
padding: 0.5rem 0.75rem;
margin-bottom: 0.5rem;
margin-bottom: 0.6rem;
}
.binary-row {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.2rem;
margin-bottom: 0.15rem;
font-size: 0.85rem;
}
.binary-label {
color: var(--vp-c-text-2);
min-width: 2.5rem;
font-weight: 600;
}
.binary-bits {
display: flex;
gap: 0.2rem;
font-family: monospace;
font-family: 'JetBrains Mono', monospace;
}
.bit {
display: inline-block;
min-width: 1.2rem;
min-width: 1.3rem;
text-align: center;
padding: 0.1rem 0.15rem;
border-radius: 3px;
transition: all 0.15s ease;
transition: all 0.15s;
}
.bit.highlight {
.bit.hl {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
font-weight: bold;
@@ -459,11 +234,11 @@ const resultDec = computed(() =>
.binary-dec {
color: var(--vp-c-text-3);
font-size: 0.8rem;
font-size: 0.78rem;
margin-left: 0.25rem;
}
.result-row .binary-bits {
.sum-row .binary-bits {
font-weight: bold;
color: var(--vp-c-brand-1);
}
@@ -472,30 +247,31 @@ const resultDec = computed(() =>
display: flex;
gap: 0.2rem;
margin-left: 3rem;
margin-top: 0.15rem;
margin-top: 0.1rem;
}
.bit-label {
min-width: 1.2rem;
min-width: 1.3rem;
text-align: center;
font-size: 0.65rem;
font-size: 0.6rem;
color: var(--vp-c-text-3);
}
/* ── stages ── */
.stages-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.4rem;
margin-bottom: 0.5rem;
}
.stage-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.4rem;
padding: 0.45rem;
cursor: pointer;
transition: all 0.15s ease;
position: relative;
transition: all 0.15s;
}
.stage-card.active {
@@ -503,25 +279,25 @@ const resultDec = computed(() =>
box-shadow: 0 0 0 1px var(--vp-c-brand-1);
}
.stage-header {
.stage-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.25rem;
padding-bottom: 0.2rem;
margin-bottom: 0.2rem;
padding-bottom: 0.15rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.stage-pos {
font-size: 0.7rem;
font-size: 0.68rem;
font-weight: bold;
color: var(--vp-c-text-2);
}
.stage-type {
font-size: 0.65rem;
font-size: 0.6rem;
font-weight: bold;
padding: 0.1rem 0.25rem;
padding: 0.08rem 0.25rem;
border-radius: 3px;
}
@@ -538,22 +314,24 @@ const resultDec = computed(() =>
.stage-io {
display: flex;
flex-direction: column;
gap: 0.15rem;
gap: 0.1rem;
}
.io-line {
.io-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.2rem;
gap: 0.25rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.78rem;
}
.io-tag {
font-size: 0.6rem;
font-size: 0.55rem;
font-weight: bold;
padding: 0.05rem 0.2rem;
padding: 0.04rem 0.18rem;
border-radius: 2px;
color: white;
font-family: system-ui;
}
.io-tag.a { background: var(--vp-c-brand-1); }
@@ -562,59 +340,21 @@ const resultDec = computed(() =>
.io-tag.s { background: var(--vp-c-green-1, #16a34a); }
.io-tag.cout { background: #d97706; }
.io-val {
font-family: monospace;
font-size: 0.8rem;
font-weight: bold;
}
.io-val.sum {
color: var(--vp-c-green-1, #16a34a);
}
.stage-divider {
height: 1px;
background: var(--vp-c-divider);
margin: 0.2rem 0;
}
.carry-arrow {
position: absolute;
right: -0.5rem;
top: 50%;
transform: translateY(-50%);
font-size: 0.6rem;
color: #d97706;
white-space: nowrap;
}
.carry-arrow.hasCarry {
font-weight: bold;
}
.info-box {
display: flex;
align-items: flex-start;
gap: 0.35rem;
background: var(--vp-c-bg-alt);
padding: 0.6rem 0.75rem;
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.info-box .icon {
flex-shrink: 0;
.demo-caption {
font-size: 0.72rem;
color: var(--vp-c-text-3);
text-align: center;
}
@media (max-width: 600px) {
.stages-row {
grid-template-columns: repeat(2, 1fr);
}
.concepts-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,380 @@
<template>
<div class="cpu-architecture-demo">
<div class="demo-label">
CPU 核心组件与指令执行周期演示 点击"时钟脉冲"执行一个指令周期
</div>
<div class="cpu-container">
<div class="cpu-frame">
<h3 class="cpu-title">CPU (中央处理器)</h3>
<div class="components-grid">
<!-- Control Unit -->
<div
class="cu-box component"
:class="{ active: currentStage === 1 || currentStage === 2 }"
>
<div class="comp-title">控制单元 (CU)</div>
<div class="comp-state">{{ cuState }}</div>
</div>
<!-- ALU -->
<div
class="alu-box component"
:class="{ active: currentStage === 3 }"
>
<div class="comp-title">算术逻辑单元 (ALU)</div>
<div class="comp-state">{{ aluState }}</div>
</div>
<!-- Registers -->
<div
class="reg-box component"
:class="{ active: currentStage === 4 }"
>
<div class="comp-title">寄存器组</div>
<div class="reg-list">
<span class="reg">R0: {{ r0 }}</span>
<span class="reg">R1: {{ r1 }}</span>
<span class="reg">PC: {{ pc }}</span>
</div>
</div>
</div>
</div>
<!-- Memory -->
<div
class="mem-frame component"
:class="{ active: currentStage === 1 || currentStage === 4 }"
>
<h3 class="cpu-title">内存 (Memory)</h3>
<div class="mem-list">
<div class="mem-loc" :class="{ 'hl-mem': pc === 10 }">
<span class="addr">M[10]</span> 取指LOAD R0, #5
</div>
<div class="mem-loc" :class="{ 'hl-mem': pc === 11 }">
<span class="addr">M[11]</span> 译码ADD R1, R0
</div>
<div class="mem-loc" :class="{ 'hl-mem': pc === 12 }">
<span class="addr">M[12]</span> 执行ALU 计算
</div>
<div class="mem-loc" :class="{ 'hl-mem': pc === 13 }">
<span class="addr">M[13]</span> 写回将结果保存
</div>
</div>
</div>
</div>
<!-- Stages progress -->
<div class="pipeline">
<div class="stage" :class="{ active: currentStage === 1 }">
<span class="step-num">1. Fetch</span>
<span class="step-desc">取指</span>
</div>
<div class="stage" :class="{ active: currentStage === 2 }">
<span class="step-num">2. Decode</span>
<span class="step-desc">译码</span>
</div>
<div class="stage" :class="{ active: currentStage === 3 }">
<span class="step-num">3. Execute</span>
<span class="step-desc">执行</span>
</div>
<div class="stage" :class="{ active: currentStage === 4 }">
<span class="step-num">4. WriteBack</span>
<span class="step-desc">写回</span>
</div>
</div>
<div class="controls">
<button class="clock-btn" @click="nextStage">
<span class="clock-icon"></span> 给一个时钟脉冲 (Next Stage)
</button>
<button class="reset-btn" @click="reset">重置</button>
</div>
<div class="logic-explain">
<p>
当前阶段状态<strong>{{ statusMsg }}</strong>
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentStage = ref(0) // 0 = Idle, 1 = Fetch, 2 = Decode, 3 = Execute, 4 = Writeback
const cycleCount = ref(0)
const pc = ref(10)
const r0 = ref(0)
const r1 = ref(0)
const cuState = ref('等待时钟信号...')
const aluState = ref('空闲')
const instructions = [
'LOAD R0, #5',
'LOAD R1, #3',
'ADD R0, R1',
'STORE M[14], R0'
]
const statusMsg = computed(() => {
if (currentStage.value === 0)
return '系统启动,等待接收时钟脉冲开始运行程序。'
if (currentStage.value === 1)
return `CPU 内部的控制单元根据程序计数器 (PC=${pc.value}),从内存取出当前指令。`
if (currentStage.value === 2)
return `控制单元翻译指令为硬件控制信号:准备执行操作。`
if (currentStage.value === 3)
return `ALU 进行计算或控制流转移,当前在处理实际数据...`
if (currentStage.value === 4)
return `将运算结果写入寄存器组或写回内存,更新程序计数器(PC)。`
return ''
})
function nextStage() {
if (currentStage.value === 0 || currentStage.value === 4) {
currentStage.value = 1
cuState.value = `取指: 读取指令`
aluState.value = '空闲'
if (currentStage.value === 4) pc.value++
} else if (currentStage.value === 1) {
currentStage.value = 2
cuState.value = `译码: 准备相关电路`
} else if (currentStage.value === 2) {
currentStage.value = 3
cuState.value = '等待 ALU 结果'
aluState.value = '计算进行中...'
} else if (currentStage.value === 3) {
currentStage.value = 4
cuState.value = '完成'
aluState.value = '结果输出'
// Fake logic update
if (cycleCount.value === 0) r0.value = 5
if (cycleCount.value === 1) r1.value = 3
if (cycleCount.value === 2) r0.value = r0.value + r1.value
cycleCount.value++
if (pc.value >= 13) {
pc.value = 9
}
}
}
function reset() {
currentStage.value = 0
cycleCount.value = 0
pc.value = 10
r0.value = 0
r1.value = 0
cuState.value = '等待时钟信号...'
aluState.value = '空闲'
}
</script>
<style scoped>
.cpu-architecture-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.cpu-container {
display: flex;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.cpu-frame {
flex: 2;
border: 2px dashed var(--vp-c-brand-1);
border-radius: 8px;
padding: 1rem;
background: var(--vp-c-bg);
}
.mem-frame {
flex: 1;
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
background: var(--vp-c-bg-alt);
}
.cpu-title {
margin: 0 0 1rem 0;
font-size: 0.95rem;
font-weight: bold;
color: var(--vp-c-brand-1);
text-align: center;
}
.components-grid {
display: flex;
flex-direction: column;
gap: 1rem;
}
.component {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
padding: 0.8rem;
border-radius: 6px;
transition: all 0.3s ease;
}
.component.active {
background: var(--vp-c-brand-soft);
border-color: var(--vp-c-brand-1);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.comp-title {
font-size: 0.8rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.3rem;
}
.comp-state {
font-size: 0.75rem;
color: var(--vp-c-text-1);
font-family: monospace;
}
.reg-list {
display: flex;
gap: 0.5rem;
}
.reg {
font-family: monospace;
font-size: 0.75rem;
background: var(--vp-c-bg);
padding: 0.2rem 0.4rem;
border-radius: 3px;
border: 1px solid var(--vp-c-divider);
}
.mem-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
font-family: monospace;
font-size: 0.75rem;
}
.mem-loc {
padding: 0.3rem 0.5rem;
background: var(--vp-c-bg);
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
}
.mem-loc.hl-mem {
background: #fef08a;
color: #a16207;
border-color: #a16207;
font-weight: bold;
}
.addr {
color: var(--vp-c-text-3);
margin-right: 0.5rem;
}
/* Pipeline Stages */
.pipeline {
display: flex;
justify-content: space-between;
margin-bottom: 1.5rem;
background: var(--vp-c-bg);
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
overflow: hidden;
}
.stage {
flex: 1;
text-align: center;
padding: 0.5rem 0;
border-right: 1px solid var(--vp-c-divider);
transition: all 0.2s;
}
.stage:last-child {
border-right: none;
}
.stage.active {
background: var(--vp-c-brand-1);
color: white;
}
.step-num {
display: block;
font-size: 0.7rem;
font-weight: 600;
margin-bottom: 0.1rem;
}
.step-desc {
display: block;
font-size: 0.8rem;
}
/* Controls */
.controls {
display: flex;
gap: 1rem;
justify-content: center;
}
.clock-btn,
.reset-btn {
padding: 0.5rem 1rem;
border-radius: 4px;
font-size: 0.85rem;
font-weight: bold;
cursor: pointer;
}
.clock-btn {
background: var(--vp-c-brand-1);
color: white;
border: none;
}
.clock-btn:hover {
background: var(--vp-c-brand-2);
}
.reset-btn {
background: transparent;
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
}
.logic-explain {
margin-top: 1rem;
padding: 0.8rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.85rem;
text-align: center;
color: var(--vp-c-text-2);
}
@media (max-width: 600px) {
.cpu-container {
flex-direction: column;
}
}
</style>
@@ -1,303 +1,314 @@
<template>
<div class="filesystem-demo">
<div class="demo-header">
<span class="title">文件系统数据的"档案柜"</span>
<span class="subtitle">操作系统如何组织和管理文件</span>
</div>
<div class="demo-content">
<div class="fs-tree">
<div class="tree-header">
<span class="header-icon">📂</span>
<span>目录结构</span>
</div>
<div class="tree-content">
<div
v-for="item in fileTree"
:key="item.path"
class="tree-item"
:class="{ selected: selectedItem === item.path }"
:style="{ paddingLeft: (item.level * 12) + 'px' }"
@click="selectItem(item)"
>
<span class="item-icon">{{ item.icon }}</span>
<span class="item-name">{{ item.name }}</span>
</div>
</div>
</div>
<div class="fs-detail">
<div class="detail-header">
<span class="detail-icon">{{ selectedItemInfo?.icon }}</span>
<span class="detail-name">{{ selectedItemInfo?.name }}</span>
<div class="demo-wrapper">
<!-- 文件树逻辑视角 -->
<div class="logical-view">
<div class="view-title">
<span>📁 你的视角 (文件系统)</span>
<span class="subtitle">漂亮整洁的目录树</span>
</div>
<div
v-if="selectedItemInfo"
class="detail-info"
>
<div class="info-row">
<span class="info-label">类型</span>
<span class="info-value">{{ selectedItemInfo.type }}</span>
<div class="file-tree">
<div class="tree-node folder expanded">
<span class="icon">💾</span> D盘 (根目录)
</div>
<div class="info-row">
<span class="info-label">路径</span>
<span class="info-value">{{ selectedItemInfo.path }}</span>
</div>
<div
v-if="selectedItemInfo.type === '文件'"
class="info-row"
>
<span class="info-label">大小</span>
<span class="info-value">{{ selectedItemInfo.size }}</span>
</div>
<div class="info-row">
<span class="info-label">权限</span>
<span class="info-value">{{ selectedItemInfo.permission }}</span>
</div>
</div>
<div
v-if="selectedItemInfo?.type === '文件'"
class="inode-info"
>
<div class="inode-title">
inode 信息
</div>
<div class="inode-visual">
<div class="inode-block">
<span class="inode-label">inode 编号</span>
<span class="inode-value">{{ selectedItemInfo.inode }}</span>
<div class="tree-children">
<div class="tree-node folder expanded">
<span class="icon">📂</span> 照片
</div>
<div class="inode-block">
<span class="inode-label">数据块</span>
<div class="data-blocks">
<span
v-for="b in selectedItemInfo.blocks"
:key="b"
class="block"
>{{ b }}</span>
<div class="tree-children">
<div
class="tree-node file"
:class="{ active: activeFile === 'pet' }"
@click="selectFile('pet')"
>
<span class="icon">🖼</span> 宠物.jpg
<span class="size-badge">3 </span>
</div>
<div
class="tree-node file"
:class="{ active: activeFile === 'vacation' }"
@click="selectFile('vacation')"
>
<span class="icon">🖼</span> 旅游.png
<span class="size-badge">2 </span>
</div>
</div>
<div class="tree-node folder expanded">
<span class="icon">📂</span> 工作
</div>
<div class="tree-children">
<div
class="tree-node file"
:class="{ active: activeFile === 'doc' }"
@click="selectFile('doc')"
>
<span class="icon">📄</span> 总结.docx
<span class="size-badge">4 </span>
</div>
</div>
</div>
</div>
</div>
<!-- 翻译官动画 -->
<div class="translator">
<div class="arrow"></div>
<div class="badge">文件系统账本<br/>(inode表)</div>
<div class="arrow"></div>
</div>
<!-- 磁盘块物理视角 -->
<div class="physical-view">
<div class="view-title">
<span>🖨 硬盘的视角 (物理存储)</span>
<span class="subtitle">无序零散的数据块</span>
</div>
<div class="disk-grid">
<div
v-for="block in 24"
:key="block"
class="disk-block"
:class="[
getBlockOwner(block),
{ active: isBlockActive(block) }
]"
>
{{ block }}
</div>
</div>
</div>
</div>
<div class="explanation-box" v-if="activeFile">
<span v-if="activeFile === 'pet'">
💡 宠物.jpg 其实被切碎分别放在了第 3814 文件系统帮你做好了翻译你只需双击它
</span>
<span v-if="activeFile === 'vacation'">
💡 旅游.png 放在了第 56
</span>
<span v-if="activeFile === 'doc'">
💡 总结.docx 被分散存放在 10111822 如果没有文件系统你得自己背下这些数字才能打开文件
</span>
</div>
<div class="explanation-box default" v-else>
试着点击左侧的文件看看它们在硬盘里到底长什么样
</div>
<div class="info-box">
<strong>核心思想</strong>文件系统用"目录树"组织文件"inode"记录文件元数据文件名只是给人看的系统通过 inode 编号找到真正的数据
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref } from 'vue'
const selectedItem = ref('/home')
const activeFile = ref(null)
const fileTree = ref([
{ name: '/', path: '/', icon: '📁', level: 0, type: '目录', permission: 'rwxr-xr-x' },
{ name: 'home', path: '/home', icon: '📁', level: 1, type: '目录', permission: 'rwxr-xr-x' },
{ name: 'user', path: '/home/user', icon: '📁', level: 2, type: '目录', permission: 'rwxr-xr-x' },
{ name: 'documents', path: '/home/user/documents', icon: '📁', level: 3, type: '目录', permission: 'rwxr-xr-x' },
{ name: 'report.pdf', path: '/home/user/documents/report.pdf', icon: '📄', level: 4, type: '文件', size: '2.5MB', permission: 'rw-r--r--', inode: 12345, blocks: ['块1', '块2', '块3'] },
{ name: 'photos', path: '/home/user/photos', icon: '📁', level: 3, type: '目录', permission: 'rwxr-xr-x' },
{ name: 'vacation.jpg', path: '/home/user/photos/vacation.jpg', icon: '🖼️', level: 4, type: '文件', size: '4.2MB', permission: 'rw-r--r--', inode: 12346, blocks: ['块4', '块5', '块6', '块7'] },
{ name: 'etc', path: '/etc', icon: '📁', level: 1, type: '目录', permission: 'rwxr-xr-x' },
{ name: 'config.yml', path: '/etc/config.yml', icon: '⚙️', level: 2, type: '文件', size: '1.2KB', permission: 'rw-r--r--', inode: 10001, blocks: ['块8'] },
{ name: 'var', path: '/var', icon: '📁', level: 1, type: '目录', permission: 'rwxr-xr-x' },
{ name: 'log', path: '/var/log', icon: '📁', level: 2, type: '目录', permission: 'rwxr-xr-x' },
{ name: 'system.log', path: '/var/log/system.log', icon: '📝', level: 3, type: '文件', size: '128MB', permission: 'rw-r-----', inode: 20001, blocks: ['块9', '块10', '...'] }
])
// 映射关系伪造
const fileMap = {
pet: [3, 8, 14],
vacation: [5, 6],
doc: [10, 11, 18, 22]
}
const selectedItemInfo = computed(() => {
return fileTree.value.find(item => item.path === selectedItem.value)
})
const selectFile = (file) => {
activeFile.value = file
}
const selectItem = (item) => {
selectedItem.value = item.path
const getBlockOwner = (block) => {
for (const [key, blocks] of Object.entries(fileMap)) {
if (blocks.includes(block)) return `owner-${key}`
}
return 'empty'
}
const isBlockActive = (block) => {
if (!activeFile.value) return false
return fileMap[activeFile.value].includes(block)
}
</script>
<style scoped>
.filesystem-demo {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-content {
.demo-wrapper {
display: flex;
align-items: stretch;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 1.5rem;
}
.fs-tree {
@media (max-width: 768px) {
.demo-wrapper {
flex-direction: column;
}
.translator {
transform: rotate(90deg);
margin: 1rem 0;
}
}
.logical-view, .physical-view {
flex: 1;
min-width: 250px;
background: var(--vp-c-bg);
border-radius: 6px;
overflow: hidden;
}
.tree-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: var(--vp-c-bg-alt);
font-weight: bold;
font-size: 0.85rem;
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.tree-content {
max-height: 280px;
overflow-y: auto;
}
.tree-item {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.35rem 0.5rem;
cursor: pointer;
transition: all 0.2s;
font-size: 0.85rem;
}
.tree-item:hover {
background: var(--vp-c-bg-soft);
}
.tree-item.selected {
background: var(--vp-c-brand-soft);
}
.item-icon {
font-size: 0.9rem;
}
.fs-detail {
flex: 1;
min-width: 250px;
}
.detail-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: var(--vp-c-bg-alt);
border-radius: 4px;
margin-bottom: 0.75rem;
}
.detail-icon {
font-size: 1.5rem;
}
.detail-name {
font-weight: bold;
font-size: 1rem;
}
.detail-info {
background: var(--vp-c-bg);
padding: 0.5rem;
border-radius: 4px;
margin-bottom: 0.75rem;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 0.25rem 0;
font-size: 0.85rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
color: var(--vp-c-text-2);
}
.info-value {
font-weight: 500;
}
.inode-info {
background: var(--vp-c-bg-alt);
padding: 0.5rem;
border-radius: 4px;
}
.inode-title {
font-weight: bold;
font-size: 0.85rem;
margin-bottom: 0.5rem;
}
.inode-visual {
.view-title {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px dashed var(--vp-c-divider);
}
.inode-block {
background: var(--vp-c-bg);
padding: 0.5rem;
border-radius: 4px;
}
.inode-label {
display: block;
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.25rem;
}
.inode-value {
.view-title span {
font-weight: bold;
font-size: 0.95rem;
}
.view-title .subtitle {
font-size: 0.75rem;
color: var(--vp-c-text-3);
font-weight: normal;
margin-top: 0.2rem;
}
/* File Tree Styles */
.file-tree {
font-size: 0.9rem;
}
.data-blocks {
display: flex;
gap: 0.25rem;
flex-wrap: wrap;
}
.block {
padding: 0.15rem 0.4rem;
background: var(--vp-c-brand-soft);
border-radius: 3px;
font-size: 0.75rem;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
.tree-node {
padding: 0.4rem 0.5rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
gap: 0.25rem;
align-items: center;
gap: 0.5rem;
cursor: pointer;
transition: all 0.2s;
}
.tree-node:hover {
background: var(--vp-c-bg-mute);
}
.tree-node.file.active {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
font-weight: bold;
}
.tree-children {
padding-left: 1.5rem;
border-left: 1px dashed var(--vp-c-divider);
margin-left: 0.6rem;
}
.size-badge {
margin-left: auto;
font-size: 0.7rem;
background: var(--vp-c-bg-mute);
padding: 0.1rem 0.4rem;
border-radius: 4px;
color: var(--vp-c-text-2);
}
.tree-node.active .size-badge {
background: var(--vp-c-brand-1);
color: white;
}
/* Translator */
.translator {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.translator .badge {
background: var(--vp-c-brand-1);
color: white;
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.8rem;
font-weight: bold;
text-align: center;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
}
.arrow {
width: 2px;
height: 20px;
background: var(--vp-c-divider);
position: relative;
}
.arrow::after {
content: '';
position: absolute;
bottom: -4px;
left: -4px;
border-width: 5px;
border-style: solid;
border-color: var(--vp-c-divider) transparent transparent transparent;
}
/* Disk Grid */
.disk-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.4rem;
}
.disk-block {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
color: var(--vp-c-text-3);
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.disk-block.owner-pet { background: rgba(16, 185, 129, 0.1); border-color: rgba(16, 185, 129, 0.3); }
.disk-block.owner-vacation { background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.3); }
.disk-block.owner-doc { background: rgba(245, 158, 11, 0.1); border-color: rgba(245, 158, 11, 0.3); }
.disk-block.active {
transform: scale(1.1);
color: white;
font-weight: bold;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
z-index: 2;
}
.disk-block.owner-pet.active { background: var(--vp-c-success-1); border-color: var(--vp-c-success-1); }
.disk-block.owner-vacation.active { background: var(--vp-c-brand-1); border-color: var(--vp-c-brand-1); }
.disk-block.owner-doc.active { background: var(--vp-c-warning-1); border-color: var(--vp-c-warning-1); }
.explanation-box {
padding: 1rem;
background: rgba(16, 185, 129, 0.1);
border-left: 4px solid var(--vp-c-success-1);
border-radius: 0 8px 8px 0;
font-size: 0.95rem;
animation: fadeIn 0.3s;
}
.explanation-box.default {
background: var(--vp-c-bg-alt);
border-left-color: var(--vp-c-text-3);
color: var(--vp-c-text-2);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
</style>
@@ -0,0 +1,369 @@
<template>
<div class="functional-unit-demo">
<div class="demo-label">
常见功能单元 切换不同模块查看其实际工作原理
</div>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
class="tab-btn"
:class="{ active: currentTab === tab.id }"
@click="currentTab = tab.id"
>
{{ tab.name }}
</button>
</div>
<div class="demo-content">
<!-- MUX Demo -->
<div v-if="currentTab === 'mux'" class="demo-panel">
<div class="panel-desc">
<strong>多路选择器 (MUX)</strong>像铁路道岔一样根据"选择信号"决定让哪一路数据通过
</div>
<div class="mux-container">
<div class="inputs">
<div class="input-line">
<span class="label">数据 0 (D0)</span>
<button
class="toggle-btn"
:class="{ on: muxD0 }"
@click="muxD0 = !muxD0"
>
{{ muxD0 ? '1' : '0' }}
</button>
</div>
<div class="input-line">
<span class="label">数据 1 (D1)</span>
<button
class="toggle-btn"
:class="{ on: muxD1 }"
@click="muxD1 = !muxD1"
>
{{ muxD1 ? '1' : '0' }}
</button>
</div>
</div>
<div class="mux-chip">
<div class="chip-body">MUX</div>
<div class="select-pin">
<span class="label">选择 (Sel)</span>
<button
class="select-btn"
:class="{ on: muxSel }"
@click="muxSel = !muxSel"
>
{{ muxSel ? '1' : '0' }}
</button>
</div>
</div>
<div class="outputs">
<div class="output-line" :class="{ active: muxResult }">
<span class="label">输出 (Out)</span>
<span class="out-val">{{ muxResult ? '1' : '0' }}</span>
</div>
</div>
</div>
<div class="logic-explain">
<p>
当前选择信号为 {{ muxSel ? '1' : '0' }}因此输出等于 数据
{{ muxSel ? '1 (D1)' : '0 (D0)' }} 的值<strong>{{
muxResult ? '1' : '0'
}}</strong>
</p>
</div>
</div>
<!-- Decoder Demo -->
<div v-if="currentTab === 'decoder'" class="demo-panel">
<div class="panel-desc">
<strong>译码器 (Decoder)</strong>将二进制输入转换为特定输出线的激活信号例如 2位输入可以激活
4根输出线中的一根
</div>
<div class="decoder-container">
<div class="inputs vertical">
<div class="input-line">
<button
class="toggle-btn"
:class="{ on: decA1 }"
@click="decA1 = !decA1"
>
{{ decA1 ? '1' : '0' }}
</button>
<span class="label">A1 (高位)</span>
</div>
<div class="input-line">
<button
class="toggle-btn"
:class="{ on: decA0 }"
@click="decA0 = !decA0"
>
{{ decA0 ? '1' : '0' }}
</button>
<span class="label">A0 (低位)</span>
</div>
</div>
<div class="decoder-chip">
<div class="chip-body">2-to-4<br />译码器</div>
</div>
<div class="outputs vertical-out">
<div class="output-line" :class="{ active: decResult === 0 }">
<span class="out-val">{{ decResult === 0 ? '1' : '0' }}</span>
<span class="label">Y0 (当输入 00 )</span>
</div>
<div class="output-line" :class="{ active: decResult === 1 }">
<span class="out-val">{{ decResult === 1 ? '1' : '0' }}</span>
<span class="label">Y1 (当输入 01 )</span>
</div>
<div class="output-line" :class="{ active: decResult === 2 }">
<span class="out-val">{{ decResult === 2 ? '1' : '0' }}</span>
<span class="label">Y2 (当输入 10 )</span>
</div>
<div class="output-line" :class="{ active: decResult === 3 }">
<span class="out-val">{{ decResult === 3 ? '1' : '0' }}</span>
<span class="label">Y3 (当输入 11 )</span>
</div>
</div>
</div>
<div class="logic-explain">
<p>
当前输入为二进制的 {{ decA1 ? '1' : '0'
}}{{ decA0 ? '1' : '0' }} (十进制 {{ decResult }})因此只有
<strong>Y{{ decResult }}</strong> 被激活输出 1
</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const tabs = [
{ id: 'mux', name: '多路选择器 (MUX)' },
{ id: 'decoder', name: '译码器 (Decoder)' }
]
const currentTab = ref('mux')
// MUX State
const muxD0 = ref(false)
const muxD1 = ref(true)
const muxSel = ref(false)
const muxResult = computed(() => (muxSel.value ? muxD1.value : muxD0.value))
// Decoder State
const decA1 = ref(false)
const decA0 = ref(false)
const decResult = computed(() => (decA1.value ? 2 : 0) + (decA0.value ? 1 : 0))
</script>
<style scoped>
.functional-unit-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
padding-bottom: 0.5rem;
}
.tab-btn {
padding: 0.4rem 0.8rem;
font-size: 0.85rem;
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s;
}
.tab-btn:hover {
border-color: var(--vp-c-brand-1);
}
.tab-btn.active {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
border-color: var(--vp-c-brand-1);
font-weight: bold;
}
.panel-desc {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
}
/* common elements */
.toggle-btn {
width: 2rem;
height: 2rem;
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
font-weight: bold;
font-family: monospace;
cursor: pointer;
transition: all 0.2s;
}
.toggle-btn.on {
background: var(--vp-c-green-soft, #dcfce7);
color: var(--vp-c-green-1, #16a34a);
border-color: var(--vp-c-green-1, #16a34a);
}
.out-val {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
font-weight: bold;
font-family: monospace;
}
.output-line.active .out-val {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
border-color: var(--vp-c-brand-1);
}
.output-line.active .label {
color: var(--vp-c-brand-1);
font-weight: bold;
}
.logic-explain {
margin-top: 1rem;
padding: 0.8rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.85rem;
text-align: center;
color: var(--vp-c-text-2);
}
/* MUX Layout */
.mux-container {
display: flex;
align-items: center;
justify-content: center;
gap: 2rem;
padding: 1rem;
}
.inputs {
display: flex;
flex-direction: column;
gap: 1rem;
}
.input-line {
display: flex;
align-items: center;
gap: 0.5rem;
}
.label {
font-size: 0.8rem;
color: var(--vp-c-text-3);
font-variant-numeric: tabular-nums;
}
.mux-chip {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.chip-body {
width: 4rem;
height: 6rem;
background: var(--vp-c-bg-alt);
border: 2px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
clip-path: polygon(0 0, 100% 20%, 100% 80%, 0 100%);
}
.select-pin {
position: absolute;
bottom: -2.5rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
}
.select-btn {
width: 2rem;
height: 1.5rem;
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
font-size: 0.8rem;
cursor: pointer;
}
.select-btn.on {
background: #fef08a; /* yellow soft */
color: #a16207;
border-color: #a16207;
}
/* Decoder Layout */
.decoder-container {
display: flex;
align-items: center;
justify-content: center;
gap: 2rem;
padding: 1rem;
}
.inputs.vertical,
.outputs.vertical-out {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.decoder-chip .chip-body {
width: 5rem;
height: 8rem;
background: var(--vp-c-bg-alt);
border: 2px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
text-align: center;
clip-path: none;
border-radius: 4px;
}
</style>
@@ -1,56 +1,56 @@
<template>
<div class="logic-gate-demo">
<div class="demo-header">
<span class="title">逻辑门用开关做运算</span>
<div class="demo-label">四种基本逻辑门 真值表一览</div>
<div class="gates-grid">
<div v-for="gate in gates" :key="gate.name" class="gate-card">
<div class="gate-name">{{ gate.name }}</div>
<div class="gate-rule">{{ gate.rule }}</div>
<table class="mini-truth">
<thead>
<tr>
<th>A</th>
<th v-if="gate.name !== 'NOT'">B</th>
<th>结果</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, i) in gate.rows" :key="i">
<td>{{ row[0] }}</td>
<td v-if="gate.name !== 'NOT'">{{ row[1] }}</td>
<td class="result-cell" :class="{ one: row[row.length - 1] === 1 }">{{ row[row.length - 1] }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<p class="intro">
输入 AB 只能是 0 1四种门按不同规则输出一个 0 1下面表格列出所有 4 种输入组合的结果
</p>
<div class="truth-section">
<table>
<thead>
<tr>
<th>A</th>
<th>B</th>
<th>AND</th>
<th>OR</th>
<th>NOT(A)</th>
<th>XOR</th>
</tr>
</thead>
<tbody>
<tr v-for="row in truthRows" :key="`${row.a}-${row.b}`">
<td>{{ row.a }}</td>
<td>{{ row.b }}</td>
<td>{{ row.and }}</td>
<td>{{ row.or }}</td>
<td>{{ row.not }}</td>
<td>{{ row.xor }}</td>
</tr>
</tbody>
</table>
<ul class="col-meaning">
<li><strong>AND</strong>两个都是 1 才输出 1像串联都通才通</li>
<li><strong>OR</strong>有一个 1 就输出 1像并联一通就通</li>
<li><strong>NOT(A)</strong> A 取反0110</li>
<li><strong>XOR</strong>两个不同输出 1相同输出 0</li>
</ul>
</div>
<div class="info-box">
<strong>核心思想</strong>逻辑门用晶体管的开关组合实现这四种运算复杂计算都由它们组合而成
</div>
<div class="demo-caption">所有数字计算都由这四种门的组合实现</div>
</div>
</template>
<script setup>
const truthRows = [
{ a: 0, b: 0, and: 0, or: 0, not: 1, xor: 0 },
{ a: 0, b: 1, and: 0, or: 1, not: 1, xor: 1 },
{ a: 1, b: 0, and: 0, or: 1, not: 0, xor: 1 },
{ a: 1, b: 1, and: 1, or: 1, not: 0, xor: 0 }
const gates = [
{
name: 'AND',
rule: '都为 1 才得 1',
rows: [[0,0,0],[0,1,0],[1,0,0],[1,1,1]]
},
{
name: 'OR',
rule: '有一个 1 就得 1',
rows: [[0,0,0],[0,1,1],[1,0,1],[1,1,1]]
},
{
name: 'NOT',
rule: '取反',
rows: [[0,1],[1,0]]
},
{
name: 'XOR',
rule: '不同才得 1',
rows: [[0,0,0],[0,1,1],[1,0,1],[1,1,0]]
}
]
</script>
@@ -59,82 +59,81 @@ const truthRows = [
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-header {
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.gates-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
}
.gate-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.6rem;
text-align: center;
}
.gate-name {
font-weight: bold;
font-size: 0.9rem;
color: var(--vp-c-brand-1);
margin-bottom: 0.15rem;
}
.gate-rule {
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-bottom: 0.5rem;
}
.demo-header .title {
font-weight: bold;
font-size: 1rem;
}
.intro {
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin: 0 0 0.75rem;
line-height: 1.5;
}
.truth-section {
margin-bottom: 0;
}
table {
.mini-truth {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
font-size: 0.88rem;
margin-bottom: 0.75rem;
font-size: 0.8rem;
font-variant-numeric: tabular-nums;
}
th,
td {
.mini-truth th,
.mini-truth td {
border: 1px solid var(--vp-c-divider);
padding: 0.4rem 0.5rem;
vertical-align: middle;
padding: 0.2rem 0.3rem;
text-align: center;
font-variant-numeric: tabular-nums;
}
th {
.mini-truth th {
background: var(--vp-c-bg-alt);
font-size: 0.72rem;
font-weight: 600;
}
.col-meaning {
margin: 0;
padding-left: 1.25rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
.col-meaning li {
margin-bottom: 0.25rem;
.result-cell.one {
color: var(--vp-c-brand-1);
font-weight: bold;
}
.col-meaning strong {
color: var(--vp-c-text-1);
font-variant-numeric: tabular-nums;
.demo-caption {
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.6rem;
text-align: center;
}
.info-box {
display: flex;
gap: 0.25rem;
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
@media (max-width: 600px) {
.gates-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
@@ -1,80 +1,65 @@
<template>
<div class="memory-demo">
<div class="demo-header">
<span class="title">内存管理程序的"工作台"</span>
<span class="subtitle">操作系统如何分配和管理内存</span>
<div class="demo-controls">
<button class="allocate-btn wechat" @click="allocate('wechat')" :disabled="!hasFreeSpace">
+ 给微信分配数据
</button>
<button class="allocate-btn game" @click="allocate('game')" :disabled="!hasFreeSpace">
+ 给游戏分配数据
</button>
<button class="reset-btn" @click="reset">
重置
</button>
</div>
<div class="demo-content">
<div class="memory-visual">
<div class="mem-header">
<span>虚拟内存空间 (4GB)</span>
<span class="used-info">已用: {{ usedMemory }}MB / 4096MB</span>
</div>
<div class="mem-blocks">
<div
v-for="(block, i) in memoryBlocks"
:key="i"
class="mem-block"
:class="{ allocated: block.allocated, selected: selectedBlock === i }"
:style="{ height: block.size + '%' }"
@click="selectedBlock = i"
>
<span
v-if="block.size > 5"
class="block-label"
>{{ block.name }}</span>
<span
v-if="block.size > 8"
class="block-size"
>{{ block.sizeMB }}MB</span>
</div>
</div>
</div>
<div class="memory-info">
<div class="info-section">
<div class="section-title">
内存分配策略
</div>
<div class="strategy-tabs">
<button
v-for="s in strategies"
:key="s.name"
:class="['strat-btn', { active: activeStrategy === s.name }]"
@click="activeStrategy = s.name"
>
{{ s.name }}
</button>
</div>
<div class="strategy-desc">
{{ currentStrategy.desc }}
<div class="system-view">
<!-- 虚拟内存试图 -->
<div class="virtual-cluster">
<div class="process-vm wechat">
<div class="title">💬 微信的虚拟内存<br/>(它认为自己独占了空间)</div>
<div class="vm-blocks">
<div v-for="i in 4" :key="'w'+i" class="block" :class="{ filled: wechatBlocks >= i }">
{{ wechatBlocks >= i ? '数据 ' + i : '虚拟空闲' }}
</div>
</div>
</div>
<div class="info-section">
<div class="section-title">
虚拟内存的作用
</div>
<div class="vm-benefits">
<div
v-for="b in benefits"
:key="b.title"
class="benefit-item"
>
<span class="benefit-icon">{{ b.icon }}</span>
<div class="benefit-content">
<span class="benefit-title">{{ b.title }}</span>
<span class="benefit-desc">{{ b.desc }}</span>
</div>
<div class="process-vm game">
<div class="title">🎮 游戏的虚拟内存<br/>(它也认为自己独占了空间)</div>
<div class="vm-blocks">
<div v-for="i in 4" :key="'g'+i" class="block" :class="{ filled: gameBlocks >= i }">
{{ gameBlocks >= i ? '数据 ' + i : '虚拟空闲' }}
</div>
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>虚拟内存让每个进程都以为自己独占整个内存空间实际由操作系统统一管理和映射实现隔离和保护
<!-- OS 页表 (映射表) -->
<div class="os-page-table">
<div class="title">保安大叔 (OS 页表)</div>
<div class="table-info">
当程序存数据时<br/>由我暗中转移到真正的物理缝隙里
</div>
</div>
<!-- 物理内存 -->
<div class="physical-memory">
<div class="title">🗄 真实的物理内存条<br/>(其实像个大杂烩一样乱)</div>
<div class="pm-blocks">
<div
v-for="(block, idx) in physicalBlocks"
:key="'p'+idx"
class="block"
:class="[block.type, { occupied: block.type !== 'empty' }]"
>
{{ block.label }}
</div>
</div>
</div>
</div>
<div class="explanation-box" v-if="wechatBlocks > 0 || gameBlocks > 0">
💡 发现了没尽管右侧真正的物理内存已经被塞得像个狗皮膏药但在左侧的微信和游戏眼里自己的内存条永远是连续且干净的更重要的是微信绝对访问不到橘色的物理块保证了安全
</div>
</div>
</template>
@@ -82,215 +67,273 @@
<script setup>
import { ref, computed } from 'vue'
const selectedBlock = ref(0)
const activeStrategy = ref('首次适应')
const wechatBlocks = ref(0)
const gameBlocks = ref(0)
const memoryBlocks = ref([
{ name: '内核空间', size: 25, allocated: true, sizeMB: 1024 },
{ name: '进程A', size: 15, allocated: true, sizeMB: 600 },
{ name: '空闲', size: 5, allocated: false, sizeMB: 200 },
{ name: '进程B', size: 20, allocated: true, sizeMB: 800 },
{ name: '空闲', size: 10, allocated: false, sizeMB: 400 },
{ name: '进程C', size: 10, allocated: true, sizeMB: 400 },
{ name: '空闲', size: 15, allocated: false, sizeMB: 600 }
])
const strategies = [
{ name: '首次适应', desc: '从内存开始找,找到第一个足够大的空闲块就分配。速度快,但可能产生小碎片。' },
{ name: '最佳适应', desc: '找最小的能满足需求的空闲块。内存利用率高,但可能产生很多小碎片。' },
{ name: '最坏适应', desc: '找最大的空闲块分配。减少小碎片,但大块内存很快用完。' }
//
// empty = , os =
const initialPhysicalBlocks = [
{ type: 'os', label: '系统核心占用' },
{ type: 'empty', label: '空闲' },
{ type: 'os', label: '系统保留' },
{ type: 'empty', label: '空闲' },
{ type: 'empty', label: '空闲' },
{ type: 'empty', label: '空闲' },
{ type: 'os', label: '系统驱动' },
{ type: 'empty', label: '空闲' },
]
const benefits = [
{ icon: '🔒', title: '内存隔离', desc: '进程间互不干扰,一个崩溃不影响其他' },
{ icon: '📦', title: '内存保护', desc: '防止进程访问不该访问的内存区域' },
{ icon: '💾', title: '内存扩展', desc: '用磁盘当内存用,突破物理内存限制' }
]
const physicalBlocks = ref(JSON.parse(JSON.stringify(initialPhysicalBlocks)))
const currentStrategy = computed(() => {
return strategies.find(s => s.name === activeStrategy.value)
const freeSpaceCount = computed(() => {
return physicalBlocks.value.filter(b => b.type === 'empty').length
})
const usedMemory = computed(() => {
return memoryBlocks.value
.filter(b => b.allocated)
.reduce((sum, b) => sum + b.sizeMB, 0)
})
const hasFreeSpace = computed(() => freeSpaceCount.value > 0)
const allocate = (process) => {
if (!hasFreeSpace.value) return
// Find a process block logic
if (process === 'wechat' && wechatBlocks.value < 4) {
wechatBlocks.value++
fillRandomEmptyBlock('wechat', `微信数据 ${wechatBlocks.value}`)
} else if (process === 'game' && gameBlocks.value < 4) {
gameBlocks.value++
fillRandomEmptyBlock('game', `游戏数据 ${gameBlocks.value}`)
}
}
const fillRandomEmptyBlock = (type, label) => {
const emptyIndices = []
physicalBlocks.value.forEach((b, i) => {
if (b.type === 'empty') emptyIndices.push(i)
})
if (emptyIndices.length > 0) {
const randomIndex = emptyIndices[Math.floor(Math.random() * emptyIndices.length)]
physicalBlocks.value[randomIndex] = { type, label }
}
}
const reset = () => {
wechatBlocks.value = 0
gameBlocks.value = 0
physicalBlocks.value = JSON.parse(JSON.stringify(initialPhysicalBlocks))
}
</script>
<style scoped>
.memory-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-content {
.demo-controls {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
justify-content: center;
flex-wrap: wrap;
}
.memory-visual {
flex: 1;
min-width: 200px;
.allocate-btn {
color: white;
border: none;
padding: 0.6rem 1.2rem;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.allocate-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
filter: grayscale(1);
}
.allocate-btn.wechat {
background: var(--vp-c-success-1);
}
.allocate-btn.wechat:not(:disabled):hover {
filter: brightness(1.1);
}
.allocate-btn.game {
background: var(--vp-c-warning-1);
}
.allocate-btn.game:not(:disabled):hover {
filter: brightness(1.1);
}
.mem-header {
.reset-btn {
background: transparent;
color: var(--vp-c-text-2);
border: 1px solid var(--vp-c-divider);
padding: 0.6rem 1.2rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.reset-btn:hover {
background: var(--vp-c-bg-mute);
color: var(--vp-c-text-1);
}
.system-view {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
margin-bottom: 0.5rem;
padding: 0.25rem 0.5rem;
background: var(--vp-c-bg-alt);
border-radius: 4px;
align-items: stretch;
gap: 1.5rem;
}
.used-info {
color: var(--vp-c-brand);
@media (max-width: 768px) {
.system-view {
flex-direction: column;
}
}
.title {
font-size: 0.85rem;
font-weight: bold;
text-align: center;
margin-bottom: 1rem;
color: var(--vp-c-text-1);
min-height: 2.5rem;
}
.mem-blocks {
.virtual-cluster {
display: flex;
gap: 1rem;
flex: 2;
}
.process-vm {
flex: 1;
background: var(--vp-c-bg-alt);
border: 2px dashed var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
}
.vm-blocks {
display: flex;
flex-direction: column;
gap: 2px;
height: 250px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
overflow: hidden;
gap: 0.5rem;
}
.mem-block {
.block {
padding: 0.6rem;
border-radius: 6px;
text-align: center;
font-size: 0.8rem;
font-weight: bold;
transition: all 0.3s;
}
.process-vm .block {
background: var(--vp-c-bg-mute);
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-3);
opacity: 0.5;
}
.process-vm.wechat .block.filled {
background: rgba(16, 185, 129, 0.15);
border: 1px solid var(--vp-c-success-1);
color: var(--vp-c-success-1);
opacity: 1;
}
.process-vm.game .block.filled {
background: rgba(245, 158, 11, 0.15);
border: 1px solid var(--vp-c-warning-1);
color: var(--vp-c-warning-1);
opacity: 1;
}
.os-page-table {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
background: var(--vp-c-bg-alt);
border-radius: 10px;
padding: 1rem;
position: relative;
border: 2px solid var(--vp-c-brand-1);
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.mem-block.allocated {
background: var(--vp-c-brand-soft);
}
.mem-block:not(.allocated) {
background: var(--vp-c-bg);
border: 1px dashed var(--vp-c-divider);
}
.mem-block.selected {
outline: 2px solid var(--vp-c-brand);
}
.block-label {
font-size: 0.75rem;
font-weight: bold;
}
.block-size {
font-size: 0.65rem;
color: var(--vp-c-text-2);
}
.memory-info {
flex: 1;
min-width: 280px;
}
.info-section {
margin-bottom: 1rem;
}
.section-title {
font-weight: bold;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.strategy-tabs {
display: flex;
gap: 0.25rem;
margin-bottom: 0.5rem;
}
.strat-btn {
padding: 0.25rem 0.5rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 4px;
font-size: 0.75rem;
cursor: pointer;
}
.strat-btn.active {
background: var(--vp-c-brand);
color: white;
border-color: var(--vp-c-brand);
}
.strategy-desc {
.os-page-table .table-info {
font-size: 0.8rem;
color: var(--vp-c-text-2);
text-align: center;
background: var(--vp-c-bg);
padding: 0.5rem;
border-radius: 4px;
padding: 0.8rem;
border-radius: 8px;
}
.vm-benefits {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.benefit-item {
display: flex;
gap: 0.5rem;
padding: 0.5rem;
background: var(--vp-c-bg);
border-radius: 4px;
}
.benefit-icon {
font-size: 1.2rem;
}
.benefit-content {
display: flex;
flex-direction: column;
}
.benefit-title {
font-weight: bold;
font-size: 0.85rem;
}
.benefit-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.info-box {
.physical-memory {
flex: 1;
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
gap: 0.25rem;
border-radius: 10px;
padding: 1rem;
border: 2px solid var(--vp-c-text-3);
}
.pm-blocks {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.pm-blocks .block {
padding: 0.5rem;
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
color: var(--vp-c-text-3);
font-size: 0.75rem;
}
.pm-blocks .block.os {
background: var(--vp-c-bg-mute);
color: var(--vp-c-text-2);
border-style: dashed;
}
.pm-blocks .block.wechat {
background: var(--vp-c-success-1);
color: white;
border-color: var(--vp-c-success-1);
animation: popIn 0.3s ease-out;
}
.pm-blocks .block.game {
background: var(--vp-c-warning-1);
color: white;
border-color: var(--vp-c-warning-1);
animation: popIn 0.3s ease-out;
}
@keyframes popIn {
0% { transform: scale(0.9); opacity: 0; }
50% { transform: scale(1.05); }
100% { transform: scale(1); opacity: 1; }
}
.explanation-box {
margin-top: 1.5rem;
padding: 1rem;
background: rgba(16, 185, 129, 0.1);
border-left: 4px solid var(--vp-c-success-1);
border-radius: 0 8px 8px 0;
font-size: 0.95rem;
animation: fadeIn 0.5s;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
@@ -1,272 +1,356 @@
<template>
<div class="process-demo">
<div class="demo-header">
<span class="title">进程程序的"分身术"</span>
<span class="subtitle">一个程序如何同时运行多个实例</span>
<div class="controls-section">
<button class="action-btn" :class="{ active: isRunning }" @click="toggleSimulation">
{{ isRunning ? '⏸ 暂停时间片轮转' : '▶️ 启动 CPU' }}
</button>
<div class="speed-control">
<label>时间流速:</label>
<button :class="{ active: speed === 'slow' }" @click="setSpeed('slow')">极慢动作</button>
<button :class="{ active: speed === 'fast' }" @click="setSpeed('fast')">真实速度</button>
</div>
</div>
<div class="demo-content">
<div class="process-list">
<div class="process-header">
<span class="col-name">进程名</span>
<span class="col-pid">PID</span>
<span class="col-state">状态</span>
<span class="col-mem">内存</span>
<div class="cpu-container">
<div class="cpu-core" :class="{ active: isRunning }">
<div class="cpu-title">单核 CPU</div>
<div class="current-task">
<span v-if="activeProcess" class="task-badge">
正在处理: {{ activeProcess.icon }} {{ activeProcess.name }}
</span>
<span v-else class="task-badge idle">
空闲中...
</span>
</div>
</div>
<!-- 连接线动画 -->
<div class="connector">
<div
v-for="p in processes"
:key="p.pid"
class="process-item"
:class="{ running: p.state === '运行中', selected: selectedPid === p.pid }"
@click="selectedPid = p.pid"
>
<span class="col-name">
<span class="process-icon">{{ p.icon }}</span>
{{ p.name }}
</span>
<span class="col-pid">{{ p.pid }}</span>
<span class="col-state">
<span
class="state-badge"
:class="p.state === '运行中' ? 'running' : 'waiting'"
>
{{ p.state }}
</span>
</span>
<span class="col-mem">{{ p.memory }}</span>
</div>
</div>
<div
v-if="selectedProcess"
class="process-detail"
>
<div class="detail-title">
进程详情{{ selectedProcess.name }}
</div>
<div class="detail-grid">
<div class="detail-item">
<span class="label">进程ID (PID)</span>
<span class="value">{{ selectedProcess.pid }}</span>
</div>
<div class="detail-item">
<span class="label">父进程ID</span>
<span class="value">{{ selectedProcess.ppid }}</span>
</div>
<div class="detail-item">
<span class="label">内存占用</span>
<span class="value">{{ selectedProcess.memory }}</span>
</div>
<div class="detail-item">
<span class="label">CPU 占用</span>
<span class="value">{{ selectedProcess.cpu }}%</span>
</div>
</div>
<div class="memory-layout">
<div class="layout-title">
进程内存布局
</div>
<div class="layout-visual">
<div
v-for="seg in memorySegments"
:key="seg.name"
class="segment"
:style="{ height: seg.height }"
>
<span class="seg-name">{{ seg.name }}</span>
</div>
</div>
class="data-flow"
:class="[ `flow-${activeProcessId}`, { running: isRunning }]">
</div>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>进程是程序的"运行实例"同一个程序可以启动多个进程每个进程有独立的内存空间互不干扰
<div class="processes-grid">
<div
v-for="p in processes"
:key="p.id"
class="process-card"
:class="{ active: p.id === activeProcessId }"
>
<div class="p-header">
<div class="p-title">
<span class="icon">{{ p.icon }}</span>
<span class="name">{{ p.name }}</span>
</div>
<span class="status-badge" :class="p.id === activeProcessId ? 'running' : 'waiting'">
{{ p.id === activeProcessId ? '独占 CPU' : '排队等待' }}
</span>
</div>
<div class="p-progress">
<div class="progress-track">
<div class="progress-fill" :style="{ width: p.progress + '%' }"></div>
</div>
<div class="progress-text">{{ Math.floor(p.progress) }}% 完成</div>
</div>
</div>
</div>
<div class="explanation-box" :class="{ show: isRunning && speed === 'fast' }">
💡 **关键启示**当切换速度足够快时肉眼已经无法分辨谁在等待这也就是为什么只有一个 CPU 核心的电脑依然能让你一边听歌一边打字
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onUnmounted } from 'vue'
const selectedPid = ref(1001)
const isRunning = ref(false)
const activeProcessId = ref(null)
const speed = ref('slow')
let interval = null
const processes = ref([
{ pid: 1001, name: 'Chrome', icon: '🌐', state: '运行中', memory: '512MB', cpu: 15, ppid: 1 },
{ pid: 1002, name: 'VS Code', icon: '📝', state: '运行中', memory: '384MB', cpu: 8, ppid: 1 },
{ pid: 1003, name: '微信', icon: '💬', state: '等待中', memory: '256MB', cpu: 2, ppid: 1 },
{ pid: 1004, name: '终端', icon: '⬛', state: '等待中', memory: '32MB', cpu: 0, ppid: 1002 },
{ pid: 1005, name: '音乐', icon: '🎵', state: '运行中', memory: '128MB', cpu: 3, ppid: 1 }
{ id: 1, name: '微信接收', icon: '💬', progress: 0 },
{ id: 2, name: '音乐播放', icon: '🎵', progress: 0 },
{ id: 3, name: '游戏渲染', icon: '🎮', progress: 0 }
])
const selectedProcess = computed(() => {
return processes.value.find(p => p.pid === selectedPid.value)
})
const activeProcess = computed(() => processes.value.find(p => p.id === activeProcessId.value))
const memorySegments = [
{ name: '栈区 (Stack)', height: '20%' },
{ name: '堆区 (Heap)', height: '35%' },
{ name: '数据段 (Data)', height: '15%' },
{ name: '代码段 (Text)', height: '30%' }
]
const setSpeed = (s) => {
speed.value = s
if (isRunning.value) {
clearInterval(interval)
startLoop()
}
}
const startLoop = () => {
const switchTime = speed.value === 'slow' ? 1200 : 80; // 1.2s
if (!activeProcessId.value) {
activeProcessId.value = 1
}
interval = setInterval(() => {
//
const curr = processes.value.find(p => p.id === activeProcessId.value)
if (curr) {
curr.progress += (speed.value === 'slow' ? 15 : 4)
if (curr.progress >= 100) curr.progress = 0
}
//
let nextId = activeProcessId.value + 1
if (nextId > 3) nextId = 1
activeProcessId.value = nextId
}, switchTime)
}
const toggleSimulation = () => {
if (isRunning.value) {
clearInterval(interval)
isRunning.value = false
activeProcessId.value = null
} else {
isRunning.value = true
startLoop()
}
}
onUnmounted(() => {
if (interval) clearInterval(interval)
})
</script>
<style scoped>
.process-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
font-family: var(--vp-font-family-base);
}
.demo-header {
.controls-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
flex-wrap: wrap;
gap: 1rem;
}
.action-btn {
background: var(--vp-c-brand-1);
color: white;
border: none;
padding: 0.6rem 1.2rem;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
min-width: 160px;
}
.action-btn.active {
background: var(--vp-c-danger-1);
}
.action-btn:hover {
filter: brightness(1.1);
transform: translateY(-1px);
}
.speed-control {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
font-size: 0.9rem;
color: var(--vp-c-text-2);
}
.demo-header .title { font-weight: bold; font-size: 1rem; }
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
.demo-content {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.process-list {
flex: 1;
min-width: 280px;
font-size: 0.85rem;
}
.process-header {
display: grid;
grid-template-columns: 2fr 1fr 1.5fr 1fr;
gap: 0.5rem;
padding: 0.5rem;
background: var(--vp-c-bg-alt);
border-radius: 4px;
font-weight: bold;
margin-bottom: 0.25rem;
}
.process-item {
display: grid;
grid-template-columns: 2fr 1fr 1.5fr 1fr;
gap: 0.5rem;
padding: 0.5rem;
border-radius: 4px;
.speed-control button {
background: transparent;
border: 1px solid var(--vp-c-divider);
padding: 0.3rem 0.8rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.process-item:hover {
background: var(--vp-c-bg);
}
.process-item.selected {
.speed-control button.active {
background: var(--vp-c-brand-soft);
}
.process-icon {
margin-right: 0.25rem;
}
.state-badge {
padding: 0.1rem 0.4rem;
border-radius: 3px;
font-size: 0.75rem;
}
.state-badge.running {
background: var(--vp-c-success);
color: white;
}
.state-badge.waiting {
background: var(--vp-c-divider);
color: var(--vp-c-text-2);
}
.process-detail {
flex: 1;
min-width: 250px;
}
.detail-title {
color: var(--vp-c-brand-1);
border-color: var(--vp-c-brand-1);
font-weight: bold;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.detail-item {
background: var(--vp-c-bg);
padding: 0.5rem;
border-radius: 4px;
}
.detail-item .label {
display: block;
font-size: 0.75rem;
color: var(--vp-c-text-2);
}
.detail-item .value {
font-weight: bold;
font-size: 0.9rem;
}
.memory-layout {
background: var(--vp-c-bg-alt);
padding: 0.5rem;
border-radius: 4px;
}
.layout-title {
font-size: 0.8rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.layout-visual {
.cpu-container {
display: flex;
flex-direction: column;
gap: 2px;
height: 120px;
align-items: center;
margin-bottom: 2rem;
}
.segment {
.cpu-core {
width: 240px;
height: 90px;
background: var(--vp-c-bg-alt);
border: 2px solid var(--vp-c-divider);
border-radius: 12px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
transition: all 0.3s;
position: relative;
}
.cpu-core.active {
border-color: var(--vp-c-brand-1);
box-shadow: 0 0 20px var(--vp-c-brand-soft);
}
.cpu-title {
font-weight: 800;
font-size: 1.1rem;
color: var(--vp-c-text-1);
margin-bottom: 0.5rem;
letter-spacing: 2px;
}
.current-task {
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: var(--vp-c-brand-soft);
border-radius: 2px;
}
.task-badge {
background: var(--vp-c-brand-1);
color: white;
padding: 0.2rem 0.8rem;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 600;
}
.task-badge.idle {
background: var(--vp-c-text-3);
}
.seg-name {
font-size: 0.7rem;
/* 连接线动画占位,简化效果,用发亮的虚线替代 */
.connector {
width: 2px;
height: 30px;
background: var(--vp-c-divider);
margin-top: 5px;
position: relative;
}
.processes-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
@media (max-width: 640px) {
.processes-grid {
grid-template-columns: 1fr;
}
}
.process-card {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
padding: 1rem;
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.process-card.active {
border-color: var(--vp-c-brand-1);
transform: translateY(-2px);
box-shadow: 0 6px 16px var(--vp-c-brand-soft);
}
.process-card.active::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 4px;
background: var(--vp-c-brand-1);
}
.p-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.p-title {
display: flex;
align-items: center;
gap: 0.4rem;
font-weight: 600;
}
.status-badge {
font-size: 0.75rem;
padding: 0.1rem 0.5rem;
border-radius: 4px;
font-weight: bold;
}
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
.status-badge.waiting {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
margin-top: 0.75rem;
display: flex;
gap: 0.25rem;
}
.status-badge.running {
background: rgba(16, 185, 129, 0.15);
color: var(--vp-c-success-1);
}
.p-progress {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.progress-track {
width: 100%;
height: 8px;
background: var(--vp-c-bg-soft);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--vp-c-brand-1);
transition: width 0.1s linear;
}
.process-card.active .progress-fill {
background: var(--vp-c-success-1);
}
.progress-text {
font-size: 0.75rem;
color: var(--vp-c-text-2);
text-align: right;
font-variant-numeric: tabular-nums;
}
.explanation-box {
margin-top: 1.5rem;
padding: 1rem;
background: rgba(16, 185, 129, 0.1);
border-left: 4px solid var(--vp-c-success-1);
border-radius: 0 8px 8px 0;
font-size: 0.95rem;
opacity: 0;
transform: translateY(10px);
transition: all 0.5s ease;
}
.explanation-box.show {
opacity: 1;
transform: translateY(0);
}
</style>
@@ -1,60 +1,38 @@
<template>
<div class="register-demo">
<div class="demo-header">
<span class="title">寄存器记住一个 0 1 的小单元</span>
<span class="subtitle">只有点写入时才会把当前输入记下来平时改输入不会影响已存的值</span>
</div>
<div class="demo-label">1 位寄存器 只在"写入"时更新存储值</div>
<div class="why-what-box">
<p class="why-p">
<strong>为啥要看这个</strong>CPU 算到一半要暂时记住中间结果寄存器就是干这个的它和直接连线不同改输入不会立刻改变里面存的东西必须主动点一次写入才会更新
</p>
<p class="what-p">
<strong>这些词是啥</strong>
<span class="term">输入</span>你想写进去的 0 1
<span class="term">写入</span>点一下把当前输入锁进寄存器
<span class="term">存储值</span>寄存器里现在记着的数只有写入时才会变
<span class="term">输出</span>从寄存器读出来的数和存储值一样
</p>
</div>
<div class="control-panel">
<label class="ctrl-group">
<span class="ctrl-label">输入</span>
<div class="reg-panel">
<!-- Input -->
<div class="reg-block">
<span class="reg-title">输入</span>
<button
class="input-toggle"
class="toggle-btn"
:class="{ on: inputData === 1 }"
@click="inputData = inputData === 1 ? 0 : 1"
>
{{ inputData }}
</button>
</label>
<button class="write-btn" :class="{ flash: isWriting }" @click="writeOnce">
写入
</button>
<label class="ctrl-group">
<span class="ctrl-label">存储</span>
<span class="stored-val" :class="{ on: storedData === 1 }">{{ storedData }}</span>
</label>
<span class="ctrl-group">
<span class="ctrl-label">输出</span>
<span class="output-val" :class="{ on: storedData === 1 }">{{ storedData }}</span>
</span>
</div>
<div class="visualization-area">
<div class="flow-strip">
<span class="flow-item">输入 {{ inputData }}</span>
<span class="flow-arrow" :class="{ active: isWriting }">{{ isWriting ? '写入中 →' : '— 点「写入」才更新 →' }}</span>
<span class="flow-item flow-store" :class="{ flash: isWriting }"> {{ storedData }}</span>
>{{ inputData }}</button>
</div>
<!-- Write -->
<button class="write-btn" :class="{ flash: isWriting }" @click="writeOnce">
写入
</button>
<!-- Stored -->
<div class="reg-block">
<span class="reg-title">存储</span>
<span class="val-box" :class="{ on: storedData === 1, flash: isWriting }">{{ storedData }}</span>
</div>
<!-- Output -->
<div class="reg-block">
<span class="reg-title">输出</span>
<span class="val-box out" :class="{ on: storedData === 1 }">{{ storedData }}</span>
</div>
<p class="flow-hint">
{{ inputData !== storedData ? '输入和存储不一样:点「写入」会把当前输入记进去。' : '输入和存储已一致。' }}
</p>
</div>
<div class="info-box">
<strong>核心思想</strong>寄存器只在写入那一刻更新其余时间一直保持原来的值所以 CPU 能稳定保存中间结果
<div class="status-line">
{{ inputData !== storedData ? '⚡ 输入≠存储 → 点"写入"即可更新' : '✓ 输入与存储一致' }}
</div>
</div>
</template>
@@ -69,9 +47,7 @@ const isWriting = ref(false)
const writeOnce = () => {
isWriting.value = true
storedData.value = inputData.value
window.setTimeout(() => {
isWriting.value = false
}, 400)
setTimeout(() => { isWriting.value = false }, 400)
}
</script>
@@ -80,75 +56,44 @@ const writeOnce = () => {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-header {
margin-bottom: 0.5rem;
}
.demo-header .title {
display: block;
.demo-label {
font-size: 0.78rem;
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
display: block;
font-size: 0.82rem;
color: var(--vp-c-text-2);
font-weight: normal;
}
.why-what-box {
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.65rem 0.85rem;
margin-bottom: 0.75rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.55;
letter-spacing: 0.2px;
}
.why-what-box .why-p {
margin: 0 0 0.4rem;
}
.why-what-box .what-p {
margin: 0;
}
.why-what-box .term {
font-weight: 600;
color: var(--vp-c-text-1);
}
.control-panel {
/* ── panel ── */
.reg-panel {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.6rem;
padding: 0.5rem 0.7rem;
flex-wrap: wrap;
padding: 0.6rem 0.8rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
margin-bottom: 0.75rem;
}
.ctrl-group {
display: inline-flex;
.reg-block {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.35rem;
gap: 0.25rem;
}
.ctrl-label {
font-size: 0.82rem;
color: var(--vp-c-text-2);
.reg-title {
font-size: 0.7rem;
font-weight: 600;
color: var(--vp-c-text-3);
}
.input-toggle {
.toggle-btn {
width: 2.2rem;
height: 2.2rem;
border-radius: 6px;
@@ -156,26 +101,28 @@ const writeOnce = () => {
background: var(--vp-c-bg-alt);
font-weight: bold;
font-size: 1rem;
font-family: 'JetBrains Mono', monospace;
cursor: pointer;
transition: all 0.2s;
}
.input-toggle.on {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
.toggle-btn.on {
border-color: var(--vp-c-brand-1);
color: var(--vp-c-brand-1);
background: var(--vp-c-brand-soft);
}
.write-btn {
padding: 0.35rem 0.8rem;
padding: 0.35rem 0.7rem;
border-radius: 6px;
border: 2px solid var(--vp-c-warning-1, #d97706);
background: var(--vp-c-bg);
color: var(--vp-c-warning-1, #d97706);
font-size: 0.85rem;
font-size: 0.82rem;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.write-btn:hover {
@@ -187,93 +134,44 @@ const writeOnce = () => {
color: white;
}
.stored-val,
.output-val {
.val-box {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2rem;
height: 2rem;
padding: 0 0.4rem;
width: 2.2rem;
height: 2.2rem;
border-radius: 6px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg-alt);
font-weight: bold;
font-family: monospace;
font-family: 'JetBrains Mono', monospace;
font-size: 1rem;
transition: all 0.2s;
}
.stored-val.on,
.output-val.on {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
.val-box.on {
border-color: var(--vp-c-brand-1);
color: var(--vp-c-brand-1);
background: var(--vp-c-brand-soft);
}
.visualization-area {
margin-bottom: 0.75rem;
.val-box.flash {
box-shadow: 0 0 0 3px var(--vp-c-warning-soft, rgba(217, 119, 6, 0.25));
}
.flow-strip {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem;
padding: 0.6rem 0.8rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
font-size: 0.9rem;
.val-box.out {
border-style: dashed;
}
.flow-item {
font-weight: bold;
}
.flow-store {
color: var(--vp-c-brand);
}
.flow-store.flash {
box-shadow: 0 0 0 2px var(--vp-c-warning-1);
border-radius: 4px;
}
.flow-arrow {
.status-line {
margin-top: 0.5rem;
font-size: 0.78rem;
color: var(--vp-c-text-3);
font-size: 0.82rem;
}
.flow-arrow.active {
color: var(--vp-c-warning-1);
font-weight: bold;
}
.flow-hint {
margin: 0.4rem 0 0;
font-size: 0.82rem;
color: var(--vp-c-text-2);
}
.info-box {
display: flex;
gap: 0.25rem;
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
}
@media (max-width: 520px) {
.control-panel {
flex-direction: column;
align-items: flex-start;
@media (max-width: 480px) {
.reg-panel {
justify-content: center;
}
}
</style>
@@ -0,0 +1,168 @@
<template>
<div class="sand-demo">
<div class="demo-label">从沙子到智能 每一层都是对下一层的封装</div>
<div class="layers">
<div
v-for="(layer, i) in layers" :key="i"
class="layer-row"
:class="{ active: activeLayer === i }"
@mouseenter="activeLayer = i"
@mouseleave="activeLayer = null"
>
<div class="layer-num">{{ i + 1 }}</div>
<div class="layer-icon">{{ layer.icon }}</div>
<div class="layer-body">
<div class="layer-name">{{ layer.name }}</div>
<div class="layer-desc">{{ layer.desc }}</div>
</div>
<div class="layer-scale">{{ layer.scale }}</div>
<div v-if="i < layers.length - 1" class="arrow-down">
<span class="arrow-label">{{ layer.arrow }}</span>
</div>
</div>
</div>
<div class="demo-caption">层层抽象封装最底层的物理材料最终变成通用计算平台</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeLayer = ref(null)
const layers = [
{ icon: '🏖️', name: '沙子(硅)', desc: '地球上最丰富的元素之一,提炼出高纯度硅', scale: '原材料', arrow: '提纯 → 切割' },
{ icon: '💿', name: '硅晶圆', desc: '直径约 30cm 的单晶硅片,表面极其光滑', scale: '基底', arrow: '光刻 → 蚀刻 → 掺杂' },
{ icon: '🔌', name: '晶体管(开关)', desc: 'Gate=1 导通,Gate=0 断开,用电压控制电流', scale: '数百亿 / 芯片', arrow: '组合成逻辑电路' },
{ icon: '🔲', name: '逻辑门', desc: 'AND / OR / NOT / XOR,实现基本布尔运算', scale: '数十亿', arrow: '组合成功能模块' },
{ icon: '⚙️', name: '功能单元', desc: '加法器、寄存器、多路选择器……各司其职', scale: '数百个', arrow: '集成为处理器' },
{ icon: '🧠', name: 'CPU 核心', desc: 'ALU + 控制器 + 寄存器组,取指→解码→执行→写回', scale: '1128 核', arrow: '软件编程' },
{ icon: '🚀', name: '软件应用', desc: '操作系统 / AI / 游戏 / 网页……一切皆指令', scale: '无限可能', arrow: '' },
]
</script>
<style scoped>
.sand-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.layers {
display: flex;
flex-direction: column;
gap: 0;
}
.layer-row {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.55rem 0.7rem;
border-radius: 6px;
position: relative;
transition: all 0.15s;
cursor: default;
}
.layer-row:hover,
.layer-row.active {
background: var(--vp-c-bg);
}
.layer-num {
width: 1.4rem;
height: 1.4rem;
border-radius: 50%;
background: var(--vp-c-bg-alt);
border: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.65rem;
font-weight: bold;
color: var(--vp-c-text-3);
flex-shrink: 0;
}
.layer-row.active .layer-num {
background: var(--vp-c-brand-soft);
border-color: var(--vp-c-brand-1);
color: var(--vp-c-brand-1);
}
.layer-icon {
font-size: 1.2rem;
flex-shrink: 0;
}
.layer-body {
flex: 1;
min-width: 0;
}
.layer-name {
font-size: 0.88rem;
font-weight: bold;
color: var(--vp-c-text-1);
line-height: 1.3;
}
.layer-desc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
line-height: 1.4;
}
.layer-scale {
font-size: 0.68rem;
font-weight: 600;
color: var(--vp-c-text-3);
white-space: nowrap;
flex-shrink: 0;
background: var(--vp-c-bg-alt);
padding: 0.15rem 0.45rem;
border-radius: 4px;
}
/* ── arrow between layers ── */
.arrow-down {
position: absolute;
left: 1.1rem;
bottom: -0.55rem;
z-index: 1;
}
.arrow-label {
font-size: 0.6rem;
color: var(--vp-c-text-3);
background: var(--vp-c-bg-soft);
padding: 0 0.3rem;
white-space: nowrap;
}
.demo-caption {
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.6rem;
text-align: center;
}
@media (max-width: 600px) {
.layer-scale {
display: none;
}
}
</style>
@@ -1,47 +1,45 @@
<template>
<div class="transistor-demo">
<div class="demo-header">
<span class="title">晶体管数字世界的开关</span>
<span class="subtitle">Gate 电压决定电流能否通过</span>
</div>
<div class="demo-label">MOSFET 晶体管示意 点击切换 Gate 电压</div>
<div class="states">
<div class="state-card">
<div class="state-label">Gate = 0低电压</div>
<div class="channel-row">
<span class="terminal">源极</span>
<div class="channel-track off">
<span class="block-icon"> 断开</span>
</div>
<span class="terminal">漏极</span>
</div>
<div class="output-line">输出<strong>0</strong></div>
<div class="schematic" @click="gateOn = !gateOn">
<!-- Source terminal -->
<div class="terminal-box source">
<span class="pin-label">源极<br><span class="en">Source</span></span>
<div class="pin-wire" :class="{ active: gateOn }"></div>
</div>
<div class="state-card">
<div class="state-label">Gate = 1高电压</div>
<div class="channel-row">
<span class="terminal">源极</span>
<div class="channel-track on">
<span class="flow-dot d1" />
<span class="flow-dot d2" />
<span class="flow-dot d3" />
<span class="flow-label">导通</span>
</div>
<span class="terminal">漏极</span>
<!-- Channel -->
<div class="channel-area">
<div class="gate-indicator" :class="{ on: gateOn }">
<span class="gate-label">Gate</span>
<span class="gate-val">{{ gateOn ? '1' : '0' }}</span>
</div>
<div class="output-line">输出<strong>1</strong></div>
<div class="channel-bar" :class="{ conducting: gateOn }">
<template v-if="gateOn">
<span class="electron e1"></span>
<span class="electron e2"></span>
<span class="electron e3"></span>
</template>
<span v-else class="block-mark"></span>
</div>
<div class="channel-status">{{ gateOn ? '导通 → 输出 1' : '断开 → 输出 0' }}</div>
</div>
<!-- Drain terminal -->
<div class="terminal-box drain">
<div class="pin-wire" :class="{ active: gateOn }"></div>
<span class="pin-label">漏极<br><span class="en">Drain</span></span>
</div>
</div>
<div class="info-box">
<strong>核心思想</strong>晶体管是电控开关Gate=1 导通Gate=0 断开所有数字计算都建立在这种 0/1 开关之上
</div>
<div class="tap-hint">👆 点击切换 Gate 电压</div>
</div>
</template>
<script setup>
//
import { ref } from 'vue'
const gateOn = ref(false)
</script>
<style scoped>
@@ -49,149 +47,168 @@
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem;
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header .title {
.demo-label {
font-size: 0.78rem;
font-weight: bold;
font-size: 1rem;
}
.demo-header .subtitle {
color: var(--vp-c-text-2);
font-size: 0.85rem;
margin-left: 0.5rem;
}
.states {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.state-card {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg);
padding: 0.75rem;
}
.state-label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
}
.channel-row {
/* ── layout ── */
.schematic {
display: flex;
align-items: center;
gap: 0.5rem;
justify-content: center;
gap: 0;
}
.terminal {
font-size: 0.82rem;
/* ── terminals ── */
.terminal-box {
display: flex;
align-items: center;
gap: 0;
}
.pin-label {
font-size: 0.78rem;
font-weight: 600;
color: var(--vp-c-text-2);
flex-shrink: 0;
line-height: 1.3;
text-align: center;
}
.channel-track {
flex: 1;
min-height: 2.5rem;
.pin-label .en {
font-size: 0.65rem;
color: var(--vp-c-text-3);
font-weight: normal;
}
.pin-wire {
width: 2.5rem;
height: 3px;
background: var(--vp-c-divider);
transition: background 0.3s;
}
.pin-wire.active {
background: var(--vp-c-brand-1);
box-shadow: 0 0 6px var(--vp-c-brand-soft);
}
/* ── channel ── */
.channel-area {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.35rem;
min-width: 7rem;
}
.gate-indicator {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.25rem 0.65rem;
border-radius: 6px;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
transition: all 0.3s;
}
.gate-indicator.on {
border-color: var(--vp-c-brand-1);
background: var(--vp-c-brand-soft);
}
.gate-label {
font-size: 0.72rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.gate-val {
font-family: 'JetBrains Mono', monospace;
font-size: 1rem;
font-weight: bold;
color: var(--vp-c-text-1);
transition: color 0.3s;
}
.gate-indicator.on .gate-val {
color: var(--vp-c-brand-1);
}
.channel-bar {
width: 100%;
height: 2rem;
border: 2px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg-alt);
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
transition: all 0.3s;
}
.channel-track.off {
background: var(--vp-c-bg-alt);
.channel-bar.conducting {
background: var(--vp-c-success-soft, rgba(22, 163, 74, 0.12));
border-color: var(--vp-c-success, #16a34a);
}
.channel-track.on {
background: var(--vp-c-success-soft);
border-color: var(--vp-c-success);
}
.block-icon {
.block-mark {
font-size: 0.9rem;
font-weight: bold;
color: var(--vp-c-text-3);
}
.flow-dot {
width: 0.4rem;
height: 0.4rem;
.electron {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--vp-c-success);
background: var(--vp-c-success, #16a34a);
position: absolute;
left: -10%;
animation: flow 1.5s linear infinite;
animation: flow 1.2s linear infinite;
}
.flow-dot.d2 {
animation-delay: 0.45s;
}
.flow-dot.d3 {
animation-delay: 0.9s;
}
.flow-label {
margin-left: 0.4rem;
font-size: 0.85rem;
color: var(--vp-c-success-1);
font-weight: 600;
}
.electron.e2 { animation-delay: 0.4s; }
.electron.e3 { animation-delay: 0.8s; }
@keyframes flow {
from {
left: -10%;
}
to {
left: 105%;
}
0% { left: -8%; opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { left: 108%; opacity: 0; }
}
.output-line {
margin-top: 0.5rem;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.output-line strong {
color: var(--vp-c-brand-1);
}
.info-box {
display: flex;
gap: 0.25rem;
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
.channel-status {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-2);
transition: color 0.3s;
}
.info-box strong {
white-space: nowrap;
flex-shrink: 0;
.channel-bar.conducting + .channel-status {
color: var(--vp-c-success, #16a34a);
}
@media (max-width: 640px) {
.states {
grid-template-columns: 1fr;
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.6rem;
}
@media (max-width: 480px) {
.pin-wire { width: 1.5rem; }
.channel-area { min-width: 5rem; }
}
</style>
@@ -6,13 +6,19 @@
<div class="mode-buttons">
<button
:class="['mode-btn', { active: mode === 'serial' }]"
@click="mode = 'serial'; reset()"
@click="
mode = 'serial';
reset()
"
>
串行传输现代
</button>
<button
:class="['mode-btn', { active: mode === 'parallel' }]"
@click="mode = 'parallel'; reset()"
@click="
mode = 'parallel';
reset()
"
>
并行传输旧时代
</button>
@@ -31,7 +37,7 @@
:key="i"
class="bit"
:class="{ sent: sentBits.includes(i) }"
>{{ bit }}</span>
>{{ bit }}</span>
</div>
</div>
@@ -45,7 +51,7 @@
:key="'p' + i"
class="particle"
:style="{ left: p.progress + '%', top: '50%' }"
>{{ p.bit }}</span>
>{{ p.bit }}</span>
</div>
</div>
<div v-if="mode === 'parallel'" class="wire-group parallel-group">
@@ -55,7 +61,7 @@
v-if="parallelParticle && parallelParticle.lane === l - 1"
class="particle"
:style="{ left: parallelParticle.progress + '%', top: '50%' }"
>{{ parallelBits[l - 1] || '·' }}</span>
>{{ parallelBits[l - 1] || '·' }}</span>
</div>
</div>
</div>
@@ -69,9 +75,13 @@
v-for="(bit, i) in receivedBits"
:key="'r' + i"
class="bit received"
>{{ bit }}</span>
>{{ bit }}</span>
</div>
<div v-if="checksumResult !== null" class="checksum-badge" :class="checksumResult ? 'ok' : 'fail'">
<div
v-if="checksumResult !== null"
class="checksum-badge"
:class="checksumResult ? 'ok' : 'fail'"
>
{{ checksumResult ? '✓ 校验通过' : '✕ 校验失败' }}
</div>
</div>
@@ -85,7 +95,9 @@
</div>
<div class="status-item">
<span class="s-label">传输速率</span>
<span class="s-val">{{ mode === 'serial' ? '1 位/次' : '8 位/次' }}</span>
<span class="s-val">{{
mode === 'serial' ? '1 位/次' : '8 位/次'
}}</span>
</div>
<div class="status-item">
<span class="s-label">状态</span>
@@ -99,8 +111,10 @@
</button>
<div class="note-box">
<strong>提示等等串行不是更慢吗</strong><br>
表面上是的但现代串行接口USB 4PCIe传输频率高达每秒 <strong>数百亿次</strong>而并行线路之间会产生 <em>信号串扰Crosstalk</em>反而限制了速度所以高速接口全面转向了串行
<strong>提示等等串行不是更慢吗</strong><br />
表面上是的但现代串行接口USB 4PCIe传输频率高达每秒
<strong>数百亿次</strong>而并行线路之间会产生
<em>信号串扰Crosstalk</em>反而限制了速度所以高速接口全面转向了串行
</div>
</div>
</template>
@@ -109,7 +123,7 @@
import { ref, computed } from 'vue'
const mode = ref('serial')
const dataBits = ref([1,0,1,1,0,0,1,0]) // "Hello" first byte 0b10110010
const dataBits = ref([1, 0, 1, 1, 0, 0, 1, 0]) // "Hello" first byte 0b10110010
const sentBits = ref([])
const receivedBits = ref([])
const particles = ref([])
@@ -141,7 +155,9 @@ const statusColor = computed(() => {
return ''
})
function sleep(ms) { return new Promise(r => setTimeout(r, ms)) }
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms))
}
async function send() {
if (isSending.value) return
@@ -156,7 +172,7 @@ async function send() {
// Checksum simulation
await sleep(400)
checksumResult.value = true // always pass in demo
checksumResult.value = true // always pass in demo
isSending.value = false
}
@@ -171,7 +187,7 @@ async function sendSerial() {
p.progress = prog
await sleep(35)
}
particles.value = particles.value.filter(x => x !== p)
particles.value = particles.value.filter((x) => x !== p)
receivedBits.value.push(bit)
await sleep(30)
}
@@ -181,7 +197,10 @@ async function sendParallel() {
sentBits.value = dataBits.value.map((_, i) => i)
parallelBits.value = [...dataBits.value]
for (let prog = 0; prog <= 100; prog += 8) {
parallelParticle.value = { progress: prog, lane: Math.floor(Math.random() * 8) }
parallelParticle.value = {
progress: prog,
lane: Math.floor(Math.random() * 8)
}
await sleep(40)
}
parallelParticle.value = null
@@ -249,10 +268,17 @@ async function sendParallel() {
width: 100px;
}
.device-icon { font-size: 2rem; }
.device-label { font-size: 0.8rem; font-weight: bold; color: var(--vp-c-text-2); }
.device-icon {
font-size: 2rem;
}
.device-label {
font-size: 0.8rem;
font-weight: bold;
color: var(--vp-c-text-2);
}
.data-bits, .received-bits {
.data-bits,
.received-bits {
display: flex;
flex-wrap: wrap;
gap: 2px;
@@ -273,8 +299,15 @@ async function sendParallel() {
transition: all 0.2s;
}
.bit.sent { background: var(--vp-c-brand-soft); border-color: var(--vp-c-brand); }
.bit.received { background: #d1fae5; border-color: #059669; color: #065f46; }
.bit.sent {
background: var(--vp-c-brand-soft);
border-color: var(--vp-c-brand);
}
.bit.received {
background: #d1fae5;
border-color: #059669;
color: #065f46;
}
.checksum-badge {
margin-top: 4px;
@@ -283,8 +316,14 @@ async function sendParallel() {
border-radius: 4px;
font-weight: bold;
}
.checksum-badge.ok { background: #d1fae5; color: #065f46; }
.checksum-badge.fail { background: #fee2e2; color: #991b1b; }
.checksum-badge.ok {
background: #d1fae5;
color: #065f46;
}
.checksum-badge.fail {
background: #fee2e2;
color: #991b1b;
}
/* Wires */
.wire-container {
@@ -311,8 +350,14 @@ async function sendParallel() {
overflow: hidden;
}
.wire-group.serial .wire { height: 20px; }
.parallel-group { display: flex; flex-direction: column; gap: 2px; }
.wire-group.serial .wire {
height: 20px;
}
.parallel-group {
display: flex;
flex-direction: column;
gap: 2px;
}
.particle {
position: absolute;
@@ -337,11 +382,25 @@ async function sendParallel() {
padding: 0.6rem 0.85rem;
}
.status-item { display: flex; flex-direction: column; gap: 2px; }
.s-label { font-size: 0.72rem; color: var(--vp-c-text-3); }
.s-val { font-size: 0.88rem; font-weight: bold; }
.s-val.green { color: #059669; }
.s-val.yellow { color: #d97706; }
.status-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.s-label {
font-size: 0.72rem;
color: var(--vp-c-text-3);
}
.s-val {
font-size: 0.88rem;
font-weight: bold;
}
.s-val.green {
color: #059669;
}
.s-val.yellow {
color: #d97706;
}
.send-btn {
padding: 0.5rem 1.2rem;
@@ -356,7 +415,10 @@ async function sendParallel() {
align-self: flex-start;
}
.send-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.send-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.note-box {
background: var(--vp-c-bg-alt);
@@ -35,8 +35,10 @@ const scopes = [
const updateExplanation = () => {
const texts = {
global: '在全局作用域,只能使用全局变量 appName',
function: '在函数作用域,可以使用自己的 message 和全局的 appName(作用域链查找)',
block: '在块级作用域,可以使用自己的 greeting,以及外层的 message 和 appName'
function:
'在函数作用域,可以使用自己的 message 和全局的 appName(作用域链查找)',
block:
'在块级作用域,可以使用自己的 greeting,以及外层的 message 和 appName'
}
explanation.value = texts[activeScope.value]
}
@@ -68,13 +70,13 @@ updateExplanation()
v-for="scope in scopes"
:key="scope.id"
class="level"
:class="{ active: activeScope === scope.id, dimmed: activeScope !== scope.id }"
:class="{
active: activeScope === scope.id,
dimmed: activeScope !== scope.id
}"
:style="{ borderLeftColor: scope.color }"
>
<div
class="level-header"
:style="{ color: scope.color }"
>
<div class="level-header" :style="{ color: scope.color }">
{{ scope.name }}
</div>
<div class="level-vars">
@@ -86,10 +88,7 @@ updateExplanation()
>
<span class="var-name">{{ v.name }}</span>
<span class="var-value">= {{ v.value }}</span>
<span
v-if="!v.own"
class="var-from"
> {{ v.from }}</span>
<span v-if="!v.own" class="var-from"> {{ v.from }}</span>
</div>
</div>
</div>
@@ -97,9 +96,7 @@ updateExplanation()
<!-- 说明 -->
<div class="explanation-box">
<div class="explanation-title">
💡 当前位置可见的变量
</div>
<div class="explanation-title">💡 当前位置可见的变量</div>
<div class="explanation-text">
{{ explanation }}
</div>
@@ -86,14 +86,18 @@ const bestPractices = ref([
const codeComparisons = ref([
{
scenario: '函数返回值',
withInference: 'function add(a: number, b: number) {\n return a + b // 推断为 number\n}',
withAnnotation: 'function add(a: number, b: number): number {\n return a + b\n}',
withInference:
'function add(a: number, b: number) {\n return a + b // 推断为 number\n}',
withAnnotation:
'function add(a: number, b: number): number {\n return a + b\n}',
recommendation: '推荐使用推断'
},
{
scenario: '复杂对象',
withInference: 'const user = {\n name: "张三",\n age: 25,\n email: "test@example.com"\n} // 类型自动推断',
withAnnotation: 'interface User {\n name: string\n age: number\n email: string\n}\n\nconst user: User = { ... }',
withInference:
'const user = {\n name: "张三",\n age: 25,\n email: "test@example.com"\n} // 类型自动推断',
withAnnotation:
'interface User {\n name: string\n age: number\n email: string\n}\n\nconst user: User = { ... }',
recommendation: '复杂结构建议用接口'
}
])
@@ -107,12 +111,13 @@ const codeComparisons = ref([
<!-- 概念说明 -->
<div class="concept-section">
<div class="concept-card">
<div class="concept-icon">
🧠
</div>
<div class="concept-icon">🧠</div>
<div class="concept-content">
<h4>什么是类型推断</h4>
<p>TypeScript 很聪明它能根据你写的代码自动推断出变量的类型不需要每次都手动标注</p>
<p>
TypeScript
很聪明它能根据你写的代码自动推断出变量的类型不需要每次都手动标注
</p>
</div>
</div>
</div>
@@ -124,15 +129,16 @@ const codeComparisons = ref([
<div
v-for="example in codeExamples"
:key="example.id"
:class="['example-card', { active: currentExample.id === example.id }]"
:class="[
'example-card',
{ active: currentExample.id === example.id }
]"
@click="selectExample(example)"
>
<div class="example-code">
{{ example.code }}
</div>
<div class="example-type">
{{ example.inferredType }}
</div>
<div class="example-type"> {{ example.inferredType }}</div>
</div>
</div>
</div>
@@ -148,9 +154,7 @@ const codeComparisons = ref([
<pre><code class="typescript">{{ currentExample.code }}</code></pre>
</div>
<div class="inference-arrow">
</div>
<div class="inference-arrow"></div>
<div class="type-panel">
<div class="panel-header">
@@ -164,9 +168,7 @@ const codeComparisons = ref([
</div>
<div class="explanation">
<div class="explanation-icon">
💡
</div>
<div class="explanation-icon">💡</div>
<div class="explanation-text">
{{ currentExample.explanation }}
</div>
@@ -183,16 +185,8 @@ const codeComparisons = ref([
<!-- 操作按钮 -->
<div class="controls">
<button
class="btn-danger"
@click="tryTypeError"
>
尝试类型错误
</button>
<button
class="btn-secondary"
@click="showError = false; errorMessage = ''"
>
<button class="btn-danger" @click="tryTypeError">尝试类型错误</button>
<button class="btn-secondary" @click="showError = false; errorMessage = ''">
清除消息
</button>
</div>
@@ -210,10 +204,7 @@ const codeComparisons = ref([
{{ practice.title }}
</div>
<ul class="practice-list">
<li
v-for="(item, i) in practice.items"
:key="i"
>
<li v-for="(item, i) in practice.items" :key="i">
{{ item }}
</li>
</ul>
@@ -234,15 +225,11 @@ const codeComparisons = ref([
</div>
<div class="comparison-codes">
<div class="comparison-code">
<div class="code-label">
使用推断
</div>
<div class="code-label">使用推断</div>
<pre><code class="typescript">{{ comparison.withInference }}</code></pre>
</div>
<div class="comparison-code">
<div class="code-label">
显式注解
</div>
<div class="code-label">显式注解</div>
<pre><code class="typescript">{{ comparison.withAnnotation }}</code></pre>
</div>
</div>
@@ -265,7 +252,8 @@ const codeComparisons = ref([
background: var(--vp-c-bg);
}
h3, h4 {
h3,
h4 {
margin: 0 0 16px 0;
font-weight: 600;
color: var(--vp-c-text-1);
@@ -380,7 +368,8 @@ h4 {
}
}
.code-panel, .type-panel {
.code-panel,
.type-panel {
flex: 1;
border: 2px solid var(--vp-c-border);
border-radius: 8px;
@@ -398,7 +387,8 @@ h4 {
gap: 8px;
}
.code-icon, .type-icon {
.code-icon,
.type-icon {
font-size: 16px;
}