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:
+186
-701
@@ -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 (
|
||||
<div>
|
||||
<span>{count}</span>
|
||||
<span>{double}</span>
|
||||
</div>
|
||||
)
|
||||
}</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>
|
||||
|
||||
Reference in New Issue
Block a user