chore: save local history restorations from accidental git restore
This commit is contained in:
@@ -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">UTC(Z 后缀)或明确偏移量</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 非 0,message 说明原因'
|
||||
},
|
||||
{
|
||||
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>
|
||||
+30
-46
@@ -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">二进制加法器 ── 输入 0–15 的两个数,观察逐位计算过程</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">输入:A、B → 输出:和(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">输入:A、B、Cin → 输出:和(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>每位加法器接收 A、B 和上一位的进位,输出本位的和与传给下一位的进位。就像手算竖式"逢二进一",只是用电路自动完成。
|
||||
</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>
|
||||
|
||||
+380
@@ -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>
|
||||
+266
-255
@@ -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 其实被切碎分别放在了第 3、8、14 块。文件系统帮你做好了翻译,你只需双击它!
|
||||
</span>
|
||||
<span v-if="activeFile === 'vacation'">
|
||||
💡 旅游.png 放在了第 5、6 块。
|
||||
</span>
|
||||
<span v-if="activeFile === 'doc'">
|
||||
💡 总结.docx 被分散存放在 10、11、18、22 块,如果没有文件系统,你得自己背下这些数字才能打开文件。
|
||||
</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>
|
||||
|
||||
+369
@@ -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">
|
||||
输入 A、B 只能是 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 取反,0→1、1→0</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>
|
||||
|
||||
+168
@@ -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: '1–128 核', 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>
|
||||
+148
-131
@@ -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 4、PCIe)传输频率高达每秒 <strong>数百亿次</strong>,而并行线路之间会产生 <em>信号串扰(Crosstalk)</em>,反而限制了速度。所以高速接口全面转向了串行。
|
||||
<strong>提示:等等,串行不是更慢吗?</strong><br />
|
||||
表面上是的——但现代串行接口(USB 4、PCIe)传输频率高达每秒
|
||||
<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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user