Compare commits
2 Commits
cc3b303280
...
08a95dd25c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08a95dd25c | ||
|
|
74d03d87e4 |
@ -10,8 +10,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.13",
|
||||
"highlight.js": "^11.11.1",
|
||||
"hooks": "file:",
|
||||
"vue": "^3.5.13"
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
|
||||
18
src/App.vue
18
src/App.vue
@ -12,13 +12,27 @@ const vClick: ObjectDirective = {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
<div v-click="'myClick'">我的是div的内容</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
179
src/components/SlideLayout.vue
Normal file
179
src/components/SlideLayout.vue
Normal file
@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div class="slide-container"
|
||||
@click.right.prevent="handleNext"
|
||||
@click.left="handlePrev"
|
||||
@keyup.space="handleNext"
|
||||
tabindex="0"
|
||||
ref="container">
|
||||
<div class="animated-background">
|
||||
<div class="gradient-circle circle-1"></div>
|
||||
<div class="gradient-circle circle-2"></div>
|
||||
<div class="gradient-circle circle-3"></div>
|
||||
</div>
|
||||
<div class="slide-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const container = ref<HTMLElement | null>(null);
|
||||
|
||||
// 定义路由顺序
|
||||
const routeOrder = [
|
||||
'introduction',
|
||||
'contents',
|
||||
'hooks-concept',
|
||||
'hooks-method',
|
||||
'comparison',
|
||||
'History'
|
||||
];
|
||||
|
||||
const getCurrentRouteIndex = () => {
|
||||
return routeOrder.indexOf(route.name as string);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
const currentIndex = getCurrentRouteIndex();
|
||||
if (currentIndex < routeOrder.length - 1) {
|
||||
router.push({ name: routeOrder[currentIndex + 1] });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
const currentIndex = getCurrentRouteIndex();
|
||||
if (currentIndex > 0) {
|
||||
router.push({ name: routeOrder[currentIndex - 1] });
|
||||
}
|
||||
};
|
||||
|
||||
// 禁用右键菜单
|
||||
const handleContextMenu = (e: Event) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 设置焦点以捕获键盘事件
|
||||
container.value?.focus();
|
||||
|
||||
// 添加右键菜单事件监听
|
||||
document.addEventListener('contextmenu', handleContextMenu);
|
||||
|
||||
// 添加键盘事件监听
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.code === 'Space') {
|
||||
handleNext();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keyup', handleKeyUp);
|
||||
|
||||
// 清理事件监听
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('contextmenu', handleContextMenu);
|
||||
document.removeEventListener('keyup', handleKeyUp);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.slide-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: #fafafa;
|
||||
outline: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.animated-background {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.gradient-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(40px);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.circle-1 {
|
||||
width: 50vw;
|
||||
height: 50vw;
|
||||
background: linear-gradient(45deg, rgba(255, 140, 0, 0.3), rgba(255, 200, 0, 0.2));
|
||||
top: -25vw;
|
||||
right: -25vw;
|
||||
animation: float1 15s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.circle-2 {
|
||||
width: 40vw;
|
||||
height: 40vw;
|
||||
background: linear-gradient(45deg, rgba(255, 140, 0, 0.2), rgba(255, 160, 0, 0.1));
|
||||
bottom: -20vw;
|
||||
left: -20vw;
|
||||
animation: float2 18s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.circle-3 {
|
||||
width: 30vw;
|
||||
height: 30vw;
|
||||
background: linear-gradient(45deg, rgba(255, 180, 0, 0.2), rgba(255, 140, 0, 0.1));
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: float3 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float1 {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-15%, 15%) rotate(30deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float2 {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translate(15%, -15%) rotate(-30deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float3 {
|
||||
0%, 100% {
|
||||
transform: translate(-50%, -50%) scale(1) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(1.2) rotate(15deg);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-content {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
@ -1,5 +1,8 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
|
||||
44
src/router/index.ts
Normal file
44
src/router/index.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import IntroductionView from '../views/IntroductionView.vue'
|
||||
import ContentsView from '../views/ContentsView.vue'
|
||||
import HooksConceptView from '../views/HooksConceptView.vue'
|
||||
import HooksMethodView from '../views/HooksMethodView.vue'
|
||||
import ComparisonView from '../views/ComparisonView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'introduction',
|
||||
component: IntroductionView
|
||||
},
|
||||
{
|
||||
path: '/contents',
|
||||
name: 'contents',
|
||||
component: ContentsView
|
||||
},
|
||||
{
|
||||
path: '/hooks-concept',
|
||||
name: 'hooks-concept',
|
||||
component: HooksConceptView
|
||||
},
|
||||
{
|
||||
path: '/hooks-method',
|
||||
name: 'hooks-method',
|
||||
component: HooksMethodView
|
||||
},
|
||||
{
|
||||
path: '/comparison',
|
||||
name: 'comparison',
|
||||
component: ComparisonView
|
||||
},
|
||||
{
|
||||
path: '/history',
|
||||
name: 'History',
|
||||
component: () => import('../views/HistoryView.vue')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
||||
@ -13,6 +13,18 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
@ -23,7 +35,6 @@ a:hover {
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
@ -59,6 +70,9 @@ button:focus-visible {
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
|
||||
318
src/views/ComparisonView.vue
Normal file
318
src/views/ComparisonView.vue
Normal file
@ -0,0 +1,318 @@
|
||||
<template>
|
||||
<SlideLayout>
|
||||
<div class="content">
|
||||
<h1 class="title animate-slide-down">编程范式对比</h1>
|
||||
|
||||
<div class="comparison-container animate-fade-in">
|
||||
<!-- Hooks 方式 -->
|
||||
<div class="comparison-side hooks-side">
|
||||
<div class="side-header">
|
||||
<div class="header-icon">🎯</div>
|
||||
<h2>Hooks 方式</h2>
|
||||
</div>
|
||||
|
||||
<div class="comparison-content">
|
||||
<div class="advantages-section">
|
||||
<h3><span class="icon">✨</span>优点</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<div class="point-title">逻辑复用</div>
|
||||
<div class="point-desc">可以将组件逻辑提取到可重用的函数中,避免重复代码</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="point-title">关注点分离</div>
|
||||
<div class="point-desc">相关的逻辑可以组合在一起,而不是分散在不同的生命周期方法中</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="point-title">代码组织</div>
|
||||
<div class="point-desc">更好的代码组织方式,便于维护和测试</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="point-title">TypeScript 支持</div>
|
||||
<div class="point-desc">更好的类型推导和类型安全</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="disadvantages-section">
|
||||
<h3><span class="icon">⚠️</span>缺点</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<div class="point-title">学习曲线</div>
|
||||
<div class="point-desc">需要理解函数式编程和组合式API的概念</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="point-title">调试复杂性</div>
|
||||
<div class="point-desc">当多个 Hooks 组合使用时,调试流程可能变得复杂</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-divider">
|
||||
<div class="divider-line"></div>
|
||||
<div class="vs-badge">VS</div>
|
||||
<div class="divider-line"></div>
|
||||
</div>
|
||||
|
||||
<!-- Options API 方式 -->
|
||||
<div class="comparison-side options-side">
|
||||
<div class="side-header">
|
||||
<div class="header-icon">📝</div>
|
||||
<h2>Options API 方式</h2>
|
||||
</div>
|
||||
|
||||
<div class="comparison-content">
|
||||
<div class="advantages-section">
|
||||
<h3><span class="icon">✨</span>优点</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<div class="point-title">直观清晰</div>
|
||||
<div class="point-desc">选项式API结构清晰,易于理解</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="point-title">学习门槛低</div>
|
||||
<div class="point-desc">传统的面向对象思维,更容易上手</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="point-title">模板清晰</div>
|
||||
<div class="point-desc">数据和方法的来源一目了然</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="disadvantages-section">
|
||||
<h3><span class="icon">⚠️</span>缺点</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<div class="point-title">逻辑复用困难</div>
|
||||
<div class="point-desc">需要通过 mixins 或高阶组件实现,容易产生命名冲突</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="point-title">代码组织</div>
|
||||
<div class="point-desc">相关逻辑分散在不同的选项中,难以维护</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="point-title">类型支持</div>
|
||||
<div class="point-desc">TypeScript 支持相对较弱</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="point-title">扩展性</div>
|
||||
<div class="point-desc">随着组件复杂度增加,代码变得难以管理</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SlideLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SlideLayout from '../components/SlideLayout.vue';
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem 3rem;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #ff8c00;
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.comparison-container {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 1rem;
|
||||
height: calc(100% - 5rem);
|
||||
}
|
||||
|
||||
.comparison-side {
|
||||
flex: 1;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 16px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.comparison-side:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.hooks-side {
|
||||
background: linear-gradient(135deg, rgba(255, 140, 0, 0.1), rgba(255, 255, 255, 0.8));
|
||||
}
|
||||
|
||||
.options-side {
|
||||
background: linear-gradient(135deg, rgba(66, 184, 131, 0.1), rgba(255, 255, 255, 0.8));
|
||||
}
|
||||
|
||||
.side-header {
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.side-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.comparison-content {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.advantages-section h3,
|
||||
.disadvantages-section h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.3rem;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
li {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.hooks-side li:hover {
|
||||
background: rgba(255, 140, 0, 0.1);
|
||||
}
|
||||
|
||||
.options-side li:hover {
|
||||
background: rgba(66, 184, 131, 0.1);
|
||||
}
|
||||
|
||||
.point-title {
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.point-desc {
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.comparison-divider {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.divider-line {
|
||||
flex: 1;
|
||||
width: 2px;
|
||||
background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.1), transparent);
|
||||
}
|
||||
|
||||
.vs-badge {
|
||||
background: #ff8c00;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 1.2rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
.comparison-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.comparison-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.comparison-content::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.comparison-content::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.animate-slide-down {
|
||||
animation: slideDown 1s ease;
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 1s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
229
src/views/ContentsView.vue
Normal file
229
src/views/ContentsView.vue
Normal file
@ -0,0 +1,229 @@
|
||||
<template>
|
||||
<SlideLayout>
|
||||
<div class="content">
|
||||
<div class="contents-container animate-fade-in">
|
||||
<h1 class="title">目录</h1>
|
||||
<div class="contents-list">
|
||||
<router-link to="/history" class="content-item">
|
||||
<span class="item-number">01</span>
|
||||
<div class="item-content">
|
||||
<h2>Hooks 历史</h2>
|
||||
<p>探索 React Hooks 到 Vue Composition API 的演进历程</p>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/hooks-concept" class="content-item">
|
||||
<span class="item-number">02</span>
|
||||
<div class="item-content">
|
||||
<h2>核心概念</h2>
|
||||
<p>掌握 Hooks 的基本概念和工作原理</p>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/hooks-method" class="content-item">
|
||||
<span class="item-number">03</span>
|
||||
<div class="item-content">
|
||||
<h2>使用方法</h2>
|
||||
<p>学习 Hooks 的常用方法和最佳实践</p>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/comparison" class="content-item">
|
||||
<span class="item-number">04</span>
|
||||
<div class="item-content">
|
||||
<h2>编程范式对比</h2>
|
||||
<p>对比 Hooks 与传统编程方式的异同</p>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/tool-chain" class="content-item">
|
||||
<span class="item-number">05</span>
|
||||
<div class="item-content">
|
||||
<h2>工具链集成</h2>
|
||||
<p>探讨 Hooks 与现代前端工具链的协同</p>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/advanced-design" class="content-item">
|
||||
<span class="item-number">06</span>
|
||||
<div class="item-content">
|
||||
<h2>高阶编程设计</h2>
|
||||
<p>从 Hooks 视角理解函数式编程与组合式设计</p>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/maintainability" class="content-item">
|
||||
<span class="item-number">07</span>
|
||||
<div class="item-content">
|
||||
<h2>代码可维护性</h2>
|
||||
<p>使用 Hooks 提升代码的可读性和可维护性</p>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/scalable-arch" class="content-item">
|
||||
<span class="item-number">08</span>
|
||||
<div class="item-content">
|
||||
<h2>可扩展架构</h2>
|
||||
<p>基于 Hooks 构建灵活可扩展的应用架构</p>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SlideLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.contents-container {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
|
||||
padding: 3rem;
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.contents-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.contents-container::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.contents-container::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 140, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.contents-container::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(255, 140, 0, 0.3);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #ff8c00;
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin: 0 0 3rem 0;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.contents-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.content-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 16px;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.content-item:hover {
|
||||
transform: translateX(10px);
|
||||
background: rgba(255, 140, 0, 0.1);
|
||||
border-color: rgba(255, 140, 0, 0.2);
|
||||
}
|
||||
|
||||
.item-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #ff8c00;
|
||||
opacity: 0.5;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-content h2 {
|
||||
color: #2c3e50;
|
||||
font-size: 1.5rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.item-content p {
|
||||
color: #666;
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 2rem;
|
||||
color: #ff8c00;
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.content-item:hover .arrow {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 1s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.contents-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.contents-container {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SlideLayout from '../components/SlideLayout.vue';
|
||||
</script>
|
||||
138
src/views/HistoryView.vue
Normal file
138
src/views/HistoryView.vue
Normal file
@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<SlideLayout>
|
||||
<div class="content">
|
||||
<h1 class="title animate-slide-down">Hooks 的历史</h1>
|
||||
<div class="timeline">
|
||||
<div class="timeline-item animate-fade-in" v-for="(item, index) in history" :key="index" :style="{ animationDelay: `${index * 0.2}s` }">
|
||||
<div class="timeline-date">{{ item.date }}</div>
|
||||
<div class="timeline-content">
|
||||
<h3>{{ item.title }}</h3>
|
||||
<p>{{ item.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SlideLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SlideLayout from '../components/SlideLayout.vue';
|
||||
|
||||
const history = [
|
||||
{
|
||||
date: '2016',
|
||||
title: 'Mixins 时代',
|
||||
description: 'Vue2 主要使用 Mixins 来复用组件逻辑,但存在命名冲突和数据来源不清晰的问题。'
|
||||
},
|
||||
{
|
||||
date: '2019',
|
||||
title: 'React Hooks 发布',
|
||||
description: 'React 16.8 发布 Hooks,展示了一种全新的逻辑复用方式。'
|
||||
},
|
||||
{
|
||||
date: '2020',
|
||||
title: 'Vue3 Composition API',
|
||||
description: 'Vue3 正式发布,引入 Composition API,提供了类似 Hooks 的能力。'
|
||||
},
|
||||
{
|
||||
date: '现在',
|
||||
title: 'Hooks 生态繁荣',
|
||||
description: 'Vue 社区出现了大量优秀的 Hooks 库,极大提升了开发效率。'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #ff8c00;
|
||||
font-size: 3rem;
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background-color: #ff8c00;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 2rem;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.timeline-item:nth-child(odd) {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.timeline-date {
|
||||
width: 120px;
|
||||
padding: 0.5rem;
|
||||
background-color: #ff8c00;
|
||||
color: white;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
width: calc(50% - 80px);
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.timeline-content h3 {
|
||||
color: #ff8c00;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.animate-slide-down {
|
||||
animation: slideDown 1s ease;
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 1s ease both;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
215
src/views/HooksConceptView.vue
Normal file
215
src/views/HooksConceptView.vue
Normal file
@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<SlideLayout>
|
||||
<div class="content">
|
||||
<h1 class="title animate-slide-down">Hooks 核心概念</h1>
|
||||
|
||||
<div class="concepts animate-fade-in">
|
||||
<div class="top-row">
|
||||
<div class="concept-card what-is">
|
||||
<div class="card-icon">💡</div>
|
||||
<h2>什么是 Hooks</h2>
|
||||
<p>Hooks 是一种特殊的函数,它允许你在函数组件中"钩入" Vue 的特性。它们提供了一种在不编写 class 的情况下使用状态和其他 Vue 特性的方法。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="concept-grid">
|
||||
<div class="concept-card">
|
||||
<div class="card-icon">🎯</div>
|
||||
<h3>组合式函数</h3>
|
||||
<p>Hooks 本质上是组合式函数,它们以 "use" 开头,可以在组件之间共享和重用状态逻辑。</p>
|
||||
<div class="code-snippet">
|
||||
<code>const useCounter = () => {
|
||||
const count = ref(0)
|
||||
return { count }
|
||||
}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="concept-card">
|
||||
<div class="card-icon">🔄</div>
|
||||
<h3>响应式系统</h3>
|
||||
<p>Hooks 与 Vue 的响应式系统深度集成,可以创建和管理响应式状态。</p>
|
||||
<div class="code-snippet">
|
||||
<code>const state = reactive({
|
||||
count: 0,
|
||||
double: computed(() => state.count * 2)
|
||||
})</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="concept-card">
|
||||
<div class="card-icon">⚡</div>
|
||||
<h3>生命周期集成</h3>
|
||||
<p>Hooks 可以访问组件的生命周期,实现副作用的管理和清理。</p>
|
||||
<div class="code-snippet">
|
||||
<code>onMounted(() => {
|
||||
// 组件挂载时执行
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理工作
|
||||
})</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="concept-card">
|
||||
<div class="card-icon">🧩</div>
|
||||
<h3>组合能力</h3>
|
||||
<p>多个 Hooks 可以组合使用,创建更复杂的功能,实现关注点分离。</p>
|
||||
<div class="code-snippet">
|
||||
<code>const { user } = useUser()
|
||||
const { posts } = usePosts(user)
|
||||
const { comments } = useComments(posts)</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SlideLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SlideLayout from '../components/SlideLayout.vue';
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem 3rem;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #ff8c00;
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.concepts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
height: calc(100% - 4rem);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
height: 25%;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.what-is {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
|
||||
.what-is h2 {
|
||||
font-size: 1.8rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.what-is p {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
max-width: 80%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.concept-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1.5rem;
|
||||
height: 75%;
|
||||
}
|
||||
|
||||
.concept-card {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 16px;
|
||||
padding: 1.25rem;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.concept-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.concept-card h3 {
|
||||
color: #333;
|
||||
font-size: 1.2rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.concept-card p {
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
margin: 0 0 0.5rem 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.code-snippet {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.code-snippet code {
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.85rem;
|
||||
color: #333;
|
||||
white-space: pre;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.animate-slide-down {
|
||||
animation: slideDown 1s ease;
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 1s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
364
src/views/HooksMethodView.vue
Normal file
364
src/views/HooksMethodView.vue
Normal file
@ -0,0 +1,364 @@
|
||||
<template>
|
||||
<SlideLayout>
|
||||
<div class="content">
|
||||
<h1 class="title animate-slide-down">Hooks 使用方法</h1>
|
||||
|
||||
<div class="methods-grid animate-fade-in">
|
||||
<div class="method-card">
|
||||
<h2>状态管理</h2>
|
||||
<div class="code-example">
|
||||
<div class="code-description">
|
||||
<p>使用 ref 和 reactive 管理响应式状态</p>
|
||||
</div>
|
||||
<pre><code class="language-typescript">import { ref, reactive } from 'vue'
|
||||
|
||||
interface User {
|
||||
name: string
|
||||
age: number
|
||||
preferences: {
|
||||
theme: string
|
||||
}
|
||||
}
|
||||
|
||||
const useUserState = () => {
|
||||
const user = reactive<User>({
|
||||
name: 'John',
|
||||
age: ref(25),
|
||||
preferences: {
|
||||
theme: 'dark'
|
||||
}
|
||||
})
|
||||
|
||||
const updateUser = (data: Partial<User>) => {
|
||||
Object.assign(user, data)
|
||||
}
|
||||
|
||||
return { user, updateUser }
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-card">
|
||||
<h2>计算属性</h2>
|
||||
<div class="code-example">
|
||||
<div class="code-description">
|
||||
<p>使用 computed 创建派生状态</p>
|
||||
</div>
|
||||
<pre><code class="language-typescript">import { computed, ComputedRef } from 'vue'
|
||||
|
||||
interface User {
|
||||
age: number
|
||||
firstName: string
|
||||
lastName: string
|
||||
}
|
||||
|
||||
const useUserStatus = (user: User) => {
|
||||
const isAdult: ComputedRef<boolean> = computed(() =>
|
||||
user.age >= 18
|
||||
)
|
||||
|
||||
const fullName: ComputedRef<string> = computed(() =>
|
||||
`${user.firstName} ${user.lastName}`
|
||||
)
|
||||
|
||||
return { isAdult, fullName }
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-card">
|
||||
<h2>副作用处理</h2>
|
||||
<div class="code-example">
|
||||
<div class="code-description">
|
||||
<p>使用 watch 和生命周期钩子管理副作用</p>
|
||||
</div>
|
||||
<pre><code class="language-typescript">import { ref, Ref, watch, onMounted } from 'vue'
|
||||
|
||||
interface QueryResult<T> {
|
||||
data: Ref<T | null>
|
||||
loading: Ref<boolean>
|
||||
error: Ref<Error | null>
|
||||
}
|
||||
|
||||
const useDataFetching = <T>(query: Ref<string>): QueryResult<T> => {
|
||||
const data = ref<T | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<Error | null>(null)
|
||||
|
||||
watch(query, async (newQuery) => {
|
||||
loading.value = true
|
||||
try {
|
||||
data.value = await fetchData<T>(newQuery)
|
||||
} catch (e) {
|
||||
error.value = e as Error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 初始加载
|
||||
if (query.value) {
|
||||
watch.value()
|
||||
}
|
||||
})
|
||||
|
||||
return { data, loading, error }
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="method-card">
|
||||
<h2>组合使用</h2>
|
||||
<div class="code-example">
|
||||
<div class="code-description">
|
||||
<p>组合多个 Hooks 创建复杂功能</p>
|
||||
</div>
|
||||
<pre><code class="language-typescript">interface Post {
|
||||
id: number
|
||||
title: string
|
||||
content: string
|
||||
}
|
||||
|
||||
const useUserDashboard = (userId: Ref<number>) => {
|
||||
const { user, updateUser } = useUserState()
|
||||
const { isAdult, fullName } = useUserStatus(user)
|
||||
|
||||
const postsQuery = computed(() =>
|
||||
`/api/users/${userId.value}/posts`
|
||||
)
|
||||
const {
|
||||
data: userPosts,
|
||||
loading: postsLoading
|
||||
} = useDataFetching<Post[]>(postsQuery)
|
||||
|
||||
return {
|
||||
user,
|
||||
updateUser,
|
||||
isAdult,
|
||||
fullName,
|
||||
userPosts,
|
||||
postsLoading
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SlideLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SlideLayout from '../components/SlideLayout.vue';
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import typescript from 'highlight.js/lib/languages/typescript';
|
||||
import 'highlight.js/styles/github-dark.css';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
// 只注册 typescript 语言以减小包体积
|
||||
hljs.registerLanguage('typescript', typescript);
|
||||
|
||||
onMounted(() => {
|
||||
document.querySelectorAll('pre code').forEach((block) => {
|
||||
hljs.highlightElement(block as HTMLElement);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem 3rem;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #ff8c00;
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.methods-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1.5rem;
|
||||
height: calc(100% - 4rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.method-card {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 16px;
|
||||
padding: 1.25rem;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.method-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.method-card h2 {
|
||||
color: #333;
|
||||
font-size: 1.5rem;
|
||||
margin: 0 0 1rem 0;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.method-card h2::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -3px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: linear-gradient(to right, #ff8c00, transparent);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.code-example {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
height: calc(100% - 3rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.code-description {
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.code-description p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
background: rgba(255, 140, 0, 0.05) !important;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 140, 0, 0.1);
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 140, 0, 0.3) transparent;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 140, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(255, 140, 0, 0.3);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.85rem;
|
||||
padding: 1rem !important;
|
||||
border-radius: 8px;
|
||||
height: 100%;
|
||||
text-align: left;
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
:deep(.hljs) {
|
||||
background: transparent !important;
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
:deep(.hljs-keyword) {
|
||||
color: #ff8c00 !important;
|
||||
}
|
||||
|
||||
:deep(.hljs-string) {
|
||||
color: #42b883 !important;
|
||||
}
|
||||
|
||||
:deep(.hljs-function) {
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
:deep(.hljs-title) {
|
||||
color: #6a51b2 !important;
|
||||
}
|
||||
|
||||
:deep(.hljs-params) {
|
||||
color: #2c3e50 !important;
|
||||
}
|
||||
|
||||
:deep(.hljs-literal) {
|
||||
color: #ff8c00 !important;
|
||||
}
|
||||
|
||||
:deep(.hljs-number) {
|
||||
color: #eb5757 !important;
|
||||
}
|
||||
|
||||
:deep(.hljs-type) {
|
||||
color: #6a51b2 !important;
|
||||
}
|
||||
|
||||
:deep(.hljs-built_in) {
|
||||
color: #ff8c00 !important;
|
||||
}
|
||||
|
||||
:deep(.hljs-comment) {
|
||||
color: #999 !important;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.animate-slide-down {
|
||||
animation: slideDown 1s ease;
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 1s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
253
src/views/HooksUsageView.vue
Normal file
253
src/views/HooksUsageView.vue
Normal file
@ -0,0 +1,253 @@
|
||||
<template>
|
||||
<SlideLayout>
|
||||
<div class="content">
|
||||
<h1 class="title animate-slide-down">Hooks 的使用</h1>
|
||||
|
||||
<div class="sections animate-fade-in">
|
||||
<!-- 概念部分 -->
|
||||
<section class="concept-section">
|
||||
<h2>核心概念</h2>
|
||||
<div class="concept-cards">
|
||||
<div class="concept-card">
|
||||
<div class="card-icon">🎯</div>
|
||||
<h3>什么是 Hooks</h3>
|
||||
<p>Hooks 是一种函数式编程范式,允许我们在函数组件中使用状态和其他 Vue 特性。它们以 "use" 开头,可以提取和重用组件逻辑。</p>
|
||||
</div>
|
||||
<div class="concept-card">
|
||||
<div class="card-icon">🔄</div>
|
||||
<h3>生命周期</h3>
|
||||
<p>Hooks 遵循 Vue 组件的生命周期,但使用更直观的函数式API,如 onMounted、onUpdated、onUnmounted 等。</p>
|
||||
</div>
|
||||
<div class="concept-card">
|
||||
<div class="card-icon">🧩</div>
|
||||
<h3>组合性</h3>
|
||||
<p>多个 Hooks 可以组合使用,创建更复杂的功能。这种组合方式使代码更模块化、更易于测试和维护。</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 使用方法部分 -->
|
||||
<section class="usage-section">
|
||||
<h2>使用方法</h2>
|
||||
<div class="code-examples">
|
||||
<div class="code-card">
|
||||
<h3>基础状态管理</h3>
|
||||
<pre><code>import { ref, computed } from 'vue'
|
||||
|
||||
const useCounter = () => {
|
||||
const count = ref(0)
|
||||
const double = computed(() => count.value * 2)
|
||||
|
||||
const increment = () => count.value++
|
||||
const decrement = () => count.value--
|
||||
|
||||
return {
|
||||
count,
|
||||
double,
|
||||
increment,
|
||||
decrement
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="code-card">
|
||||
<h3>副作用处理</h3>
|
||||
<pre><code>import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const useWindowSize = () => {
|
||||
const width = ref(window.innerWidth)
|
||||
const height = ref(window.innerHeight)
|
||||
|
||||
const handleResize = () => {
|
||||
width.value = window.innerWidth
|
||||
height.value = window.innerHeight
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
return { width, height }
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="code-card">
|
||||
<h3>组合多个 Hooks</h3>
|
||||
<pre><code>const useUserProfile = () => {
|
||||
const { width } = useWindowSize()
|
||||
const { count } = useCounter()
|
||||
|
||||
const isMobile = computed(() => width.value < 768)
|
||||
const userLevel = computed(() =>
|
||||
count.value > 10 ? 'Advanced' : 'Beginner'
|
||||
)
|
||||
|
||||
return { isMobile, userLevel }
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</SlideLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SlideLayout from '../components/SlideLayout.vue';
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 2rem 4rem;
|
||||
gap: 2rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #ff8c00;
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
section h2 {
|
||||
color: #333;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
section h2::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: linear-gradient(to right, #ff8c00, transparent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 概念卡片样式 */
|
||||
.concept-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.concept-card {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
padding: 1.5rem;
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.concept-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 12px rgba(255, 140, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.concept-card h3 {
|
||||
color: #333;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.concept-card p {
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 代码示例样式 */
|
||||
.code-examples {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.code-card {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
padding: 1.5rem;
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.code-card h3 {
|
||||
color: #333;
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.code-card pre {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.code-card code {
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 0.9rem;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.animate-slide-down {
|
||||
animation: slideDown 1s ease;
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 1s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
107
src/views/IntroductionView.vue
Normal file
107
src/views/IntroductionView.vue
Normal file
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<SlideLayout>
|
||||
<div class="content">
|
||||
<div class="header animate-slide-down">
|
||||
<h1 class="title">VueHooks</h1>
|
||||
<div class="subtitle">优雅的代码复用方案</div>
|
||||
</div>
|
||||
<div class="presenter-info animate-fade-in">
|
||||
<div class="info-text">
|
||||
专业服务开发部 · 燕陇琪 · {{ formatDate() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SlideLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SlideLayout from '../components/SlideLayout.vue';
|
||||
|
||||
const formatDate = () => {
|
||||
const date = new Date('2025-01-03T12:06:33+08:00');
|
||||
return `${date.getFullYear()}年${String(date.getMonth() + 1).padStart(2, '0')}月${String(date.getDate()).padStart(2, '0')}日`;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #ff8c00;
|
||||
font-size: 8rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 2rem;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 2.5rem;
|
||||
color: #666;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.presenter-info {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 1.2rem;
|
||||
color: #666;
|
||||
letter-spacing: 1px;
|
||||
display: inline-block;
|
||||
padding: 0.5rem 2rem;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 50px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.animate-slide-down {
|
||||
animation: slideDown 1s ease;
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 1s ease 0.5s both;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
21
yarn.lock
21
yarn.lock
@ -333,6 +333,11 @@
|
||||
de-indent "^1.0.2"
|
||||
he "^1.2.0"
|
||||
|
||||
"@vue/devtools-api@^6.6.4":
|
||||
version "6.6.4"
|
||||
resolved "https://mirrors.yuchat.top/repository/npmjs/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
|
||||
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
|
||||
|
||||
"@vue/language-core@2.2.0":
|
||||
version "2.2.0"
|
||||
resolved "https://mirrors.yuchat.top/repository/npmjs/@vue/language-core/-/language-core-2.2.0.tgz#e48c54584f889f78b120ce10a050dfb316c7fcdf"
|
||||
@ -473,11 +478,18 @@ he@^1.2.0:
|
||||
resolved "https://mirrors.yuchat.top/repository/npmjs/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
highlight.js@^11.11.1:
|
||||
version "11.11.1"
|
||||
resolved "https://mirrors.yuchat.top/repository/npmjs/highlight.js/-/highlight.js-11.11.1.tgz#fca06fa0e5aeecf6c4d437239135fabc15213585"
|
||||
integrity sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==
|
||||
|
||||
"hooks@file:.":
|
||||
version "0.0.0"
|
||||
dependencies:
|
||||
hooks "file:C:/Users/Administrator/AppData/Local/Yarn/Cache/v6/npm-hooks-0.0.0-02bdcb68-002e-4d83-9f65-7dd5aa63d929-1735441352086/node_modules/hooks"
|
||||
dayjs "^1.11.13"
|
||||
hooks "file:C:/Users/yanlongqi/AppData/Local/Yarn/Cache/v6/npm-hooks-0.0.0-3c6b1486-789e-4725-9581-a6da575f3e2f-1735881618100/node_modules/hooks"
|
||||
vue "^3.5.13"
|
||||
vue-router "4"
|
||||
|
||||
magic-string@^0.30.11:
|
||||
version "0.30.17"
|
||||
@ -576,6 +588,13 @@ vscode-uri@^3.0.8:
|
||||
resolved "https://mirrors.yuchat.top/repository/npmjs/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f"
|
||||
integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==
|
||||
|
||||
vue-router@4:
|
||||
version "4.5.0"
|
||||
resolved "https://mirrors.yuchat.top/repository/npmjs/vue-router/-/vue-router-4.5.0.tgz#58fc5fe374e10b6018f910328f756c3dae081f14"
|
||||
integrity sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==
|
||||
dependencies:
|
||||
"@vue/devtools-api" "^6.6.4"
|
||||
|
||||
vue-tsc@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://mirrors.yuchat.top/repository/npmjs/vue-tsc/-/vue-tsc-2.2.0.tgz#dd06c56636f760d7534b7a7a0f6669ba93c217b8"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user