简易知乎应用

72 阅读2分钟

使用 Vite、TypeScript 、 Vue 3 Composition API、Vue Router、UnoCSS 搭建的一个简易知乎应用。

创建项目

npm init vite@latest my-zhihu-clone --template vue-ts
cd my-zhihu-clone
npm install

安装 Vue Router 和 unocss 依赖:

npm install vue-router@4
npm install unocss --save-dev

配置 UnoCSS

在 vite.config.ts 中配置 UnoCSS:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(), UnoCSS()],
})

根目录创建一个 uno.config.ts 文件来配置 UnoCSS:

import { defineConfig } from 'unocss'

export default defineConfig({
  rules: [
    // 在此处定义自定义规则
  ],
  shortcuts: {
    btn: 'px-4 py-2 rounded bg-blue-500 text-white hover:bg-blue-600',
    // 更多的快捷方式
  },
  theme: {
    colors: {
      primary: '#007bff',
      secondary: '#6c757d',
      // 更多的主题颜色
    },
  },
})

编写代码

1. 设置路由

在 src/router/index.ts 中配置路由:

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Question from '../views/Question.vue'

const routes = [
  { path: '/', component: Home },
  { path: '/question/:id', component: Question },
]

const router = createRouter({
  history: createWebHistory(),
  routes,
})

export default router

2. 主应用文件 main.ts

在 src/main.ts 中初始化 Vue 应用:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import 'uno.css'

createApp(App).use(router).mount('#app')

3. 主组件 App.vue

在 src/App.vue 中设置主要布局:

<template>
  <div id="app">
    <Header />
    <router-view class="p-6" />
  </div>
</template>

<script setup lang="ts">
import Header from './components/Header.vue'
</script>

<style>
body {
  margin: 0;
}
</style>

4. Header 组件

创建组件 src/components/Header.vue:

<template>
  <header class="bg-primary text-white p-4">
    <RouterLink to="/">
      <h1>Zhihu Clone</h1>
    </RouterLink>
  </header>
</template>

<style scoped>
a {
  color: white;
  text-decoration: none;
}
</style>

5. 抽离数据

添加模拟数据,这些数据包含图像和文本内容,数据保存在 src/data/questions.ts 文件中。

// src/data/questions.ts
export const questions = [
  {
    id: 1,
    title: 'What is Vue.js?',
    content:
      'Vue.js is a progressive JavaScript framework for building user interfaces...',
    image: 'https://niit-soft.oss-cn-hangzhou.aliyuncs.com/blog/1.jpg',
    author: {
      name: 'John Doe',
      avatar: 'https://niit-soft.oss-cn-hangzhou.aliyuncs.com/avatar/1.jpg',
    },
  },
  {
    id: 2,
    title: 'How to use TypeScript with Vue?',
    content:
      'Using TypeScript with Vue.js can greatly improve your development experience...',
    image: 'https://niit-soft.oss-cn-hangzhou.aliyuncs.com/blog/2.jpg',
    author: {
      name: 'Jane Smith',
      avatar: 'https://niit-soft.oss-cn-hangzhou.aliyuncs.com/avatar/2.jpg',
    },
  },
]

6. 子组件 QuestionCard 组件

创建 components/QuestionCard 组件,用来显示每个问题的信息,并实现父子组件通信。

<template>
  <div class="border p-4 rounded mb-4 bg-blue-100">
    <div class="flex items-center mb-2">
      <img
        :src="question.author.avatar"
        alt="avatar"
        class="w-10 h-10 rounded-full mr-2"
      />
      <span class="font-bold">{{ question.author.name }}</span>
    </div>
    <img :src="question.image" alt="question image" class="mb-2 w-100" />
    <h3 class="text-xl font-bold">{{ question.title }}</h3>
    <p>{{ question.content }}</p>
    <button class="btn mt-2" @click="viewDetails">View Details</button>
  </div>
</template>

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'

const props = defineProps({
  question: {
    type: Object,
    required: true,
  },
})

const emits = defineEmits(['view-details'])

const viewDetails = () => {
  emits('view-details', props.question.id)
}
</script>

7. 父组件 Home.vue

创建页面 src/views/Home.vue,引入 data 中的数据,并监听子组件 QuestionCard 的 view-details 事件并处理。

<template>
  <div>
    <QuestionCard
      v-for="question in questions"
      :key="question.id"
      :question="question"
      @view-details="handleViewDetails"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { questions } from '../data/questions'
import QuestionCard from '../components/QuestionCard.vue'
import { useRouter } from 'vue-router'

const questionList = ref(questions)
const router = useRouter()

const handleViewDetails = (id: number) => {
  router.push(`/question/${id}`)
}
</script>

8. 详情页

创建 src/views/Question.vue 详情页,获取路由传递的值,展示问题的详细信息。

<template>
  <div v-if="question">
    <div class="flex items-center mb-6">
      <img
        :src="question.author.avatar"
        alt="avatar"
        class="w-10 h-10 rounded-full mr-2"
      />
      <span class="font-bold">{{ question.author.name }}</span>
    </div>
    <img :src="question.image" alt="question image" class="mb-2 w-100" />
    <h3 class="text-xl font-bold">{{ question.title }}</h3>
    <p>{{ question.content }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { questions } from '../data/questions'

const route = useRoute()
const question = ref(null)

onMounted(() => {
  const id = parseInt(route.params.id as string)
  question.value = questions.find((q) => q.id === id) || null
})
</script>

运行项目

npm run dev

打开浏览器 http://localhost:5173/,将呈现一个带有数据和图文并茂的仿知乎应用,并实现了父子组件通信和基本的页面导航。