Files
test-repo/docs/.vitepress/theme/components/appendix/component-state-management/ZustandJotaiDemo.vue
T
sanbuphy 7c70c37072 feat(docs): add interactive demo components for technical appendices
Add placeholder Vue components for visualizing technical concepts across multiple domains including frontend routing, browser rendering, cache design, queue design, database principles, API design, cloud services, and backend evolution. These components provide interactive educational content for the documentation.

Update documentation structure to include new appendix sections and enhance existing content with visual components. Remove unused 'codex' dependency from package.json.
2026-02-06 03:34:50 +08:00

824 lines
18 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="zustand-jotai-demo">
<div class="demo-header">
<h4>Zustand & Jotai轻量级状态管理</h4>
<p class="hint">探索现代 React 生态中最简洁的状态管理方案体验"钩子即状态"的开发模式</p>
</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>
<!-- 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>
</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>
</div>
<div class="code-content">
<pre><code>import { atom } from 'jotai'
import { atomFamily } from 'jotai/utils'
// 基础原子
const countAtom = atom(0)
// 派生原子 - 自动追踪依赖
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
}</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>
</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>
</div>
</template>
<script setup>
import { ref, computed, watch } 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);
}
.demo-header {
margin-bottom: 20px;
}
.demo-header h4 {
margin: 0 0 8px 0;
color: var(--vp-c-text-1);
}
.hint {
margin: 0;
font-size: 14px;
color: var(--vp-c-text-2);
}
.demo-tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
border-bottom: 1px solid var(--vp-c-divider);
padding-bottom: 12px;
}
.tab-button {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
font-size: 14px;
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s ease;
}
.tab-button:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-text-1);
}
.tab-button.active {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.tab-icon {
font-size: 20px;
}
.tab-name {
font-weight: 500;
}
.split-view {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@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 {
display: flex;
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 {
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 {
text-align: center;
}
.count-number {
font-size: 36px;
font-weight: 700;
color: var(--vp-c-brand);
}
.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;
font-weight: 600;
color: #0369a1;
}
.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;
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 {
background: #1e1e1e;
border-radius: 6px;
padding: 10px;
overflow: auto;
}
.api-response pre {
margin: 0;
font-family: 'Fira Code', monospace;
font-size: 11px;
line-height: 1.5;
color: #d4d4d4;
}
.info-section {
margin-top: 20px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.info-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 16px;
}
.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;
color: var(--vp-c-text-2);
margin: 5px 0;
line-height: 1.5;
}
.info-card li strong {
color: var(--vp-c-text-1);
}
@media (max-width: 768px) {
.action-buttons {
grid-template-columns: 1fr;
}
.atom-core {
transform: scale(0.9);
}
}
</style>