feat(docs): enhance interactive demos and improve documentation

- Add new interactive components for frontend routing, browser rendering pipeline, and database transactions
- Improve existing demos with better visuals, explanations, and examples
- Update documentation structure and content for better clarity
- Add new utility scripts and update package.json with new commands
- Fix formatting and alignment in documentation tables
This commit is contained in:
sanbuphy
2026-02-13 22:10:03 +08:00
parent 599052b2e0
commit d174ceea32
88 changed files with 26273 additions and 15539 deletions
@@ -1,461 +1,198 @@
<template>
<div class="zustand-jotai-demo">
<div class="demo-header">
<h4>Zustand & Jotai轻量级状态管理</h4>
<p class="hint">探索现代 React 生态中最简洁的状态管理方案体验"钩子即状态"的开发模式</p>
<span class="icon">🐻</span>
<span class="title">Zustand & Jotai</span>
<span class="subtitle">React 轻量级状态管理</span>
</div>
<div class="demo-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
class="tab-button"
:class="{ active: activeTab === tab.id }"
@click="activeTab = tab.id"
>
<span class="tab-icon">{{ tab.icon }}</span>
<span class="tab-name">{{ tab.name }}</span>
</button>
<div class="intro-text">
想象你在<span class="highlight">便利店</span>工作Zustand 就像整个仓库统一管理Jotai 就像把商品拆成一个个小格子Atom每个格子独立管理按需取用
</div>
<!-- Zustand 演示 -->
<div v-if="activeTab === 'zustand'" class="tab-content">
<div class="split-view">
<!-- 左侧代码 -->
<div class="code-panel">
<div class="code-tabs">
<button
v-for="file in zustandFiles"
:key="file.name"
class="code-tab"
:class="{ active: activeZustandFile === file.name }"
@click="activeZustandFile = file.name"
>
{{ file.name }}
</button>
</div>
<div class="code-content">
<pre><code>{{ getZustandFileContent() }}</code></pre>
</div>
</div>
<!-- 右侧演示 -->
<div class="demo-panel">
<div class="panel-header">
<span class="header-icon">🐻</span>
<span class="header-title">Zustand Store Demo</span>
</div>
<div class="bear-counter">
<div class="bear-display">
<span v-for="n in Math.min(bears, 10)" :key="n" class="bear-icon">🐻</span>
<span v-if="bears > 10" class="more-bears">+{{ bears - 10 }}</span>
</div>
<div class="count-display">
<span class="count-number">{{ bears }}</span>
<span class="count-label">bears around here</span>
</div>
</div>
<div class="fish-tank">
<div class="tank-header">
<span>🐟 Fish Tank</span>
<span class="fish-count">{{ fishes }} fishes</span>
</div>
<div class="tank-content">
<div v-for="fish in Math.min(fishes, 8)" :key="fish" class="fish">
🐠
</div>
</div>
</div>
<div class="action-buttons">
<button class="action-btn primary" @click="addBear">
<span class="btn-icon"></span>
Add Bear
</button>
<button class="action-btn secondary" @click="addFish">
<span class="btn-icon">🐟</span>
Add Fish
</button>
<button class="action-btn danger" @click="eatFish">
<span class="btn-icon">🍽</span>
Eat Fish ({{ fishes }})
</button>
<button class="action-btn warning" @click="removeAllBears">
<span class="btn-icon">🗑</span>
Remove All
</button>
</div>
<div class="async-demo">
<div class="async-header">Async Action Demo</div>
<div class="async-controls">
<button class="async-btn" :disabled="loading" @click="fetchBears">
{{ loading ? ' Fetching...' : '🌐 Fetch Bears from API' }}
</button>
</div>
<div v-if="apiResponse" class="api-response">
<pre>{{ apiResponse }}</pre>
</div>
</div>
</div>
<div class="demo-content">
<div class="demo-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
class="tab-button"
:class="{ active: activeTab === tab.id }"
@click="activeTab = tab.id"
>
<span class="tab-icon">{{ tab.icon }}</span>
<span class="tab-name">{{ tab.name }}</span>
</button>
</div>
</div>
<!-- Jotai 演示 -->
<div v-if="activeTab === 'jotai'" class="tab-content">
<!-- Jotai 内容类似结构 -->
<div class="split-view">
<div class="code-panel">
<!-- Jotai 代码示例 -->
<div class="code-tabs">
<button class="code-tab active">atoms/counter.js</button>
<Transition name="fade" mode="out-in">
<div :key="activeTab" class="tab-content">
<div v-if="activeTab === 'zustand'" class="feature-showcase">
<div class="feature-card">
<span class="feature-icon">📦</span>
<span class="feature-title">单一 Store</span>
<span class="feature-desc">所有状态集中管理</span>
</div>
<div class="feature-card">
<span class="feature-icon"></span>
<span class="feature-title">极简 API</span>
<span class="feature-desc">无需 Provider 包裹</span>
</div>
<div class="feature-card">
<span class="feature-icon">🎯</span>
<span class="feature-title">细粒度订阅</span>
<span class="feature-desc">只重渲染需要的组件</span>
</div>
</div>
<div class="code-content">
<pre><code>import { atom } from 'jotai'
import { atomFamily } from 'jotai/utils'
// 基础原子
<div v-if="activeTab === 'zustand'" class="code-example">
<pre class="code-block"><code>// Zustand Store
import { create } from 'zustand'
const useStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({
bears: state.bears + 1
}))
}))
// 在组件中使用
function BearCounter() {
const bears = useStore((state) => state.bears)
return <div>{bears} bears around here</div>
}</code></pre>
</div>
<div v-if="activeTab === 'jotai'" class="feature-showcase">
<div class="feature-card">
<span class="feature-icon"></span>
<span class="feature-title">原子化</span>
<span class="feature-desc">状态拆分成独立 Atom</span>
</div>
<div class="feature-card">
<span class="feature-icon">🔗</span>
<span class="feature-title">自动依赖</span>
<span class="feature-desc">派生状态自动追踪</span>
</div>
<div class="feature-card">
<span class="feature-icon">📝</span>
<span class="feature-title">TypeScript</span>
<span class="feature-desc">原生类型支持</span>
</div>
</div>
<div v-if="activeTab === 'jotai'" class="code-example">
<pre class="code-block"><code>// Jotai Atom
import { atom } from 'jotai'
// 基础 Atom
const countAtom = atom(0)
// 派生原子 - 自动追踪依赖
// 派生 Atom
const doubleAtom = atom((get) => get(countAtom) * 2)
const isEvenAtom = atom((get) => get(countAtom) % 2 === 0)
// 可写派生原子
const countAndDoubleAtom = atom(
(get) => ({
count: get(countAtom),
double: get(doubleAtom)
}),
(get, set, newCount) => {
set(countAtom, newCount)
}
)
// 原子家族 - 动态创建原子
const todoAtomFamily = atomFamily((id) =>
atom({ id, text: '', completed: false })
)
// 异步原子
const userAtom = atom(null)
const fetchUserAtom = atom(
(get) => get(userAtom),
async (get, set, userId) => {
const response = await fetch(\`/api/users/\${userId}\`)
const user = await response.json()
set(userAtom, user)
}
)
export {
countAtom,
doubleAtom,
isEvenAtom,
countAndDoubleAtom,
todoAtomFamily,
fetchUserAtom
// 在组件中使用
function Counter() {
const [count, setCount] = useAtom(countAtom)
const [double] = useAtom(doubleAtom)
return (
&lt;div&gt;
&lt;span&gt;{count}&lt;/span&gt;
&lt;span&gt;{double}&lt;/span&gt;
&lt;/div&gt;
)
}</code></pre>
</div>
</div>
<div class="demo-panel">
<div class="panel-header">
<span class="header-icon">⚛️</span>
<span class="header-title">Jotai Atom Demo</span>
</div>
<div class="atom-demo">
<div class="atom-visualization">
<div class="atom-core" :class="{ active: count > 0 }">
<div class="atom-nucleus">
<span class="nucleus-label">countAtom</span>
<span class="nucleus-value">{{ count }}</span>
</div>
<div class="electron-orbits">
<div class="orbit orbit-1"></div>
<div class="orbit orbit-2"></div>
<div class="orbit orbit-3"></div>
</div>
</div>
<div class="derived-atoms">
<div class="derived-atom" :class="{ active: double > 0 }">
<div class="atom-label">doubleAtom</div>
<div class="atom-value">{{ double }}</div>
<div class="atom-formula">count × 2</div>
</div>
<div class="derived-atom" :class="{ active: true }">
<div class="atom-label">isEvenAtom</div>
<div class="atom-value" :class="{ even: isEven }">{{ isEven ? 'YES' : 'NO' }}</div>
<div class="atom-formula">count % 2 === 0</div>
</div>
</div>
</div>
<div class="atom-controls">
<div class="control-group">
<button class="control-btn" @click="increment">
<span class="btn-icon"></span>
increment()
</button>
<button class="control-btn" @click="decrement">
<span class="btn-icon"></span>
decrement()
</button>
<button class="control-btn" @click="reset">
<span class="btn-icon">🔄</span>
reset()
</button>
</div>
<div class="control-group">
<button class="control-btn async" :disabled="loading" @click="fetchRandom">
<span class="btn-icon">{{ loading ? '⏳' : '🎲' }}</span>
{{ loading ? 'Fetching...' : 'Fetch Random (Async)' }}
</button>
</div>
</div>
</div>
</div>
</div>
</Transition>
</div>
<!-- 底部说明 -->
<div class="info-section">
<div class="info-card">
<h5>🎯 核心概念</h5>
<ul>
<li><strong>Atom</strong>: 状态的基本单元,可读写</li>
<li><strong>Derived Atom</strong>: 派生状态,自动追踪依赖</li>
<li><strong>Atom Family</strong>: 动态创建原子集合</li>
</ul>
</div>
<div class="info-card">
<h5>⚡ 与 Redux/MobX 对比</h5>
<ul>
<li>更细粒度的状态管理</li>
<li>天然支持 TypeScript</li>
<li>不需要 Provider 包裹</li>
<li>与 React Suspense 配合良好</li>
</ul>
</div>
<div class="info-box">
<span class="icon">💡</span>
<strong>选择建议</strong>Zustand 适合中小项目API 简洁直观Jotai 适合需要细粒度控制的场景状态更模块化两个都支持 TypeScript不需要 Provider
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref } from 'vue'
// Tab 切换
const activeTab = ref('zustand')
const tabs = [
{ id: 'zustand', name: 'Zustand', icon: '🐻' },
{ id: 'jotai', name: 'Jotai', icon: '⚛️' }
]
// Zustand 状态
const bears = ref(0)
const fishes = ref(5)
const loading = ref(false)
const apiResponse = ref('')
// 计算属性(模拟派生状态)
const double = computed(() => bears.value * 2)
const isEven = computed(() => bears.value % 2 === 0)
// 方法
const increment = () => {
bears.value++
}
const decrement = () => {
if (bears.value > 0) bears.value--
}
const reset = () => {
bears.value = 0
fishes.value = 5
}
const addBear = () => {
bears.value++
}
const addFish = () => {
fishes.value++
}
const eatFish = () => {
if (fishes.value > 0) {
fishes.value--
bears.value += 0.1
}
}
const removeAllBears = () => {
bears.value = 0
}
const fetchBears = async () => {
loading.value = true
apiResponse.value = ''
await new Promise(resolve => setTimeout(resolve, 1500))
bears.value = Math.floor(Math.random() * 10) + 1
apiResponse.value = `{\n "status": "success",\n "data": {\n "bears": ${bears.value},\n "message": "Bears fetched successfully"\n }\n}`
loading.value = false
}
const fetchRandom = async () => {
loading.value = true
await new Promise(resolve => setTimeout(resolve, 1000))
bears.value = Math.floor(Math.random() * 100)
loading.value = false
}
// 文件切换
const zustandFiles = [
{ name: 'store.js', content: `import { create } from 'zustand'
const useStore = create((set, get) => ({
// State
bears: 0,
fishes: 5,
// Computed (in component using selectors)
// const doubleBears = useStore(state => state.bears * 2)
// Actions
increasePopulation: () =>
set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
eatFish: () => set((state) => ({
fishes: state.fishes - 1,
bears: state.bears + 0.1
})),
// Async action
fetchBears: async () => {
try {
const response = await fetch('/api/bears')
const data = await response.json()
if (data && typeof data.count === 'number') {
set({ bears: data.count })
}
} catch (error) {
console.error('Failed to fetch bears:', error)
}
},
// Get current state
getState: () => get()
}))
export default useStore` },
{ name: 'Component.jsx', content: `import useStore from './store'
// Component using Zustand
function BearCounter() {
// Select only what you need - fine-grained updates
const bears = useStore((state) => state.bears)
const increase = useStore((state) => state.increasePopulation)
return (
<div>
<h1>{bears} bears around here...</h1>
<button onClick={increase}>Add bear</button>
</div>
)
}
// Async action usage
function BearFetcher() {
const fetchBears = useStore((state) => state.fetchBears)
const bears = useStore((state) => state.bears)
return (
<div>
<p>Bears: {bears}</p>
<button onClick={fetchBears}>
Fetch Bears
</button>
</div>
)
}
// Multiple state selections
function FishTank() {
const { fishes, bears, eatFish } = useStore(
(state) => ({
fishes: state.fishes,
bears: state.bears,
eatFish: state.eatFish
})
)
return (
<div>
<p>Fishes: {fishes}</p>
<p>Bears: {bears}</p>
<button onClick={eatFish}>Eat Fish</button>
</div>
)
}` }
]
const activeZustandFile = ref('store.js')
const getZustandFileContent = () => {
return zustandFiles.find(f => f.name === activeZustandFile.value)?.content || ''
}
</script>
<style scoped>
.zustand-jotai-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
background: var(--vp-c-bg-soft);
padding: 1rem;
margin: 1rem 0;
max-height: 600px;
overflow-y: auto;
}
.demo-header {
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
.demo-header .icon {
font-size: 1.25rem;
}
.hint {
margin: 0;
font-size: 14px;
.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;
}
.intro-text {
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.6;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.intro-text .highlight {
color: var(--vp-c-brand-1);
font-weight: 500;
}
.demo-content {
margin-bottom: 1rem;
}
.demo-tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
gap: 0.75rem;
margin-bottom: 1rem;
border-bottom: 1px solid var(--vp-c-divider);
padding-bottom: 12px;
padding-bottom: 0.75rem;
}
.tab-button {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
gap: 0.5rem;
padding: 0.6rem 1.2rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
font-size: 14px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s ease;
@@ -473,351 +210,99 @@ const getZustandFileContent = () => {
}
.tab-icon {
font-size: 20px;
font-size: 1rem;
}
.tab-name {
font-weight: 500;
}
.split-view {
.tab-content {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1rem;
}
.feature-showcase {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
@media (max-width: 968px) {
.split-view {
grid-template-columns: 1fr;
}
}
.code-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
overflow: hidden;
}
.code-tabs {
display: flex;
background: var(--vp-c-bg-soft);
border-bottom: 1px solid var(--vp-c-divider);
}
.code-tab {
padding: 10px 16px;
background: transparent;
border: none;
font-size: 13px;
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s ease;
}
.code-tab:hover {
color: var(--vp-c-text-1);
background: var(--vp-c-bg);
}
.code-tab.active {
color: var(--vp-c-brand);
background: var(--vp-c-bg);
border-bottom: 2px solid var(--vp-c-brand);
}
.code-content {
max-height: 600px;
overflow: auto;
}
.code-content pre {
margin: 0;
padding: 16px;
font-family: 'Fira Code', monospace;
font-size: 12px;
line-height: 1.6;
color: #d4d4d4;
background: #1e1e1e;
}
.demo-panel {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 20px;
}
.panel-header {
.feature-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid var(--vp-c-divider);
}
.header-icon {
font-size: 24px;
}
.header-title {
font-weight: 600;
font-size: 16px;
color: var(--vp-c-text-1);
}
.bear-counter {
gap: 0.5rem;
padding: 1rem;
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
}
.bear-display {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: center;
margin-bottom: 12px;
min-height: 40px;
}
.bear-icon {
font-size: 24px;
}
.more-bears {
font-size: 14px;
color: var(--vp-c-text-2);
align-self: center;
}
.count-display {
border-radius: 8px;
text-align: center;
}
.count-number {
font-size: 36px;
font-weight: 700;
color: var(--vp-c-brand);
.feature-icon {
font-size: 2rem;
}
.count-label {
display: block;
font-size: 14px;
color: var(--vp-c-text-2);
margin-top: 4px;
}
.fish-tank {
background: linear-gradient(180deg, #e0f2fe 0%, #bae6fd 100%);
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.tank-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.feature-title {
font-weight: 600;
color: #0369a1;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.tank-content {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
min-height: 50px;
}
.fish {
font-size: 28px;
animation: swim 2s ease-in-out infinite;
}
.fish:nth-child(2) { animation-delay: 0.3s; }
.fish:nth-child(3) { animation-delay: 0.6s; }
.fish:nth-child(4) { animation-delay: 0.9s; }
@keyframes swim {
0%, 100% { transform: translateY(0) rotate(0deg); }
25% { transform: translateY(-3px) rotate(-2deg); }
75% { transform: translateY(3px) rotate(2deg); }
}
.action-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin-bottom: 16px;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.action-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.action-btn.primary {
background: var(--vp-c-brand);
color: white;
}
.action-btn.primary:hover {
background: var(--vp-c-brand-dark);
}
.action-btn.secondary {
background: #0ea5e9;
color: white;
}
.action-btn.secondary:hover {
background: #0284c7;
}
.action-btn.danger {
background: #ef4444;
color: white;
}
.action-btn.danger:hover {
background: #dc2626;
}
.action-btn.warning {
background: #f59e0b;
color: white;
}
.action-btn.warning:hover {
background: #d97706;
}
.action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-icon {
font-size: 14px;
}
.async-demo {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 12px;
}
.async-header {
font-size: 12px;
font-weight: 600;
.feature-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin-bottom: 8px;
}
.async-controls {
margin-bottom: 8px;
}
.async-btn {
width: 100%;
padding: 10px;
background: #8b5cf6;
color: white;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.async-btn:hover:not(:disabled) {
background: #7c3aed;
}
.async-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.api-response {
.code-example {
background: #1e1e1e;
border-radius: 6px;
padding: 10px;
overflow: auto;
padding: 1rem;
overflow-x: auto;
}
.api-response pre {
.code-block {
margin: 0;
font-family: 'Fira Code', monospace;
font-size: 11px;
line-height: 1.5;
}
.code-block code {
font-family: monospace;
font-size: 0.75rem;
line-height: 1.6;
color: #d4d4d4;
}
.info-section {
margin-top: 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.info-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.info-card h5 {
margin: 0 0 10px 0;
font-size: 14px;
color: var(--vp-c-text-1);
}
.info-card ul {
margin: 0;
padding-left: 18px;
}
.info-card li {
font-size: 12px;
.info-box {
background: var(--vp-c-bg-alt);
padding: 0.75rem;
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
margin: 5px 0;
line-height: 1.5;
}
.info-card li strong {
color: var(--vp-c-text-1);
.info-box .icon {
margin-right: 0.25rem;
}
@media (max-width: 768px) {
.action-buttons {
grid-template-columns: 1fr;
.demo-tabs {
flex-direction: column;
}
.atom-core {
transform: scale(0.9);
.feature-showcase {
grid-template-columns: 1fr;
}
}
</style>