Vue3.2开发一个博客Demo项目
技术选型
Vue3 + Vue-router + Pinia + ElementPlus
代码地址
https://gitee.com/galen.zhang/vue3-demo/tree/master/tech-blog
Mock后台服务器代码
https://gitee.com/galen.zhang/vue3-demo/tree/master/mock-server
cd mock-server
npm install
npm run dev
创建项目
npm create vue@latest
√ Project name: ... tech-blog
√ Add TypeScript? ... No
√ Add JSX Support? ... No
√ Add Vue Router for Single Page Application development? ... Yes
√ Add Pinia for state management? ... Yes
√ Add Vitest for Unit Testing? ... No
√ Add an End-to-End Testing Solution? >> No
√ Add ESLint for code quality? ... Yes
√ Add Prettier for code formatting? ... Yes
cd tech-blog
npm install
npm run format
npm run dev
浏览器访问
安装Element Plus
官方文档
https://element-plus.org/zh-CN/guide/installation.html
安装Element Plus
npm install element-plus --save
npm install @element-plus/icons-vue
在 src/main.js
中引入Element Plus
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(ElementPlus)
app.use(createPinia())
app.use(router)
app.mount('#app')
路由配置
修改文件 src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue')
},
{
path: '/blog/:id',
component: () => import('../views/BlogDetailView.vue')
},
{
path: '/blog/:id/edit',
component: () => import('../views/CreateEditBlogView.vue')
},
{
path: '/create',
component: () => import('../views/CreateEditBlogView.vue')
},
{
path: '/myBlog',
component: () => import('../views/MyBlogView.vue')
},
{
path: '/register',
component: () => import('../views/RegisterView.vue')
},
{
path: '/login',
component: () => import('../views/LoginView.vue')
}
]
})
export default router
封装网络请求工具类Axios
npm install axios --save
// 进度条
npm install nprogress --save
封装网络请求工具类 src/utils/request.js
import axios from 'axios'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
export const serverUrl = 'http://localhost:3000'
const service = axios.create({
baseURL: serverUrl,
timeout: 5000
})
// Add a request interceptor 全局请求拦截
service.interceptors.request.use(
function (config) {
// Do something before request is sent
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
NProgress.start() // 启动进度条
// 此处还可以设置token
return config
},
function (error) {
// Do something with request error
return Promise.reject(error)
}
)
// Add a response interceptor 全局相应拦截
service.interceptors.response.use(
function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
NProgress.done()
// 如果是固定的数据返回模式,此处可以做继续完整的封装
const resData = response.data || {}
if (resData.code == '0') {
return resData.data
}
return Promise.reject(resData.message)
},
function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
NProgress.done()
// 此处需要对返回的状态码或者异常信息作统一处理
return Promise.reject(error)
}
)
export const get = (url, params) => {
return service.get(url, {
params
})
}
export const post = (url, data) => service.post(url, data)
export const put = (url, data) => service.put(url, data)
export const del = (url, data) => service.delete(url)
后台请求api接口
博客后台请求接口api文件 src/api/blogApi.js
import { get, post, put, del } from "../utils/request";
// 获取博客列表
export async function getBlogList(opt = {}) {
const { page = 1, pageSize = 10, category = '', keyword = '', my = false } = opt;
return get(`/api/blogs?page=${page}&pageSize=${pageSize}&category=${category}&keyword=${keyword}&my=${my}`);
}
// 创建博客
export async function createBlog(data) {
return post('/api/blogs', data);
}
// 编辑博客
export async function editBlog(id, data) {
return put(`/api/blogs/${id}`, data);
}
// 删除博客
export async function deleteBlog(id) {
return delete(`/api/blogs/${id}`);
}
// 根据 ID 获取单个博客
export async function getBlogById(id) {
return get(`/api/blogs/${id}`);
}
// 点赞博客
export async function likeBlog(id) {
return post(`/api/blogs/${id}/like`);
}
// 取消点赞博客
export async function unlikeBlog(id) {
return del(`/api/blogs/${id}/like`);
}
// 收藏博客
export async function favoriteBlog(id) {
return post(`/api/blogs/${id}/favorite`);
}
// 取消收藏博客
export async function unfavoriteBlog(id) {
return del(`/api/blogs/${id}/favorite`);
}
export default {
getBlogList,
createBlog,
editBlog,
deleteBlog,
getBlogById,
likeBlog,
unlikeBlog,
favoriteBlog,
unfavoriteBlog
}
封装hooks函数
修改页面标题
添加文件 src/hooks/usePageTitle.js
import { ref, isRef, onMounted, onBeforeUnmount, watchEffect } from 'vue'
const NAME = 'TechBlog'
function usePageTitle(title) {
const originalTitle = ref(document.title)
// 更新网页标题
const updatePageTitle = () => {
const titleValue = isRef(title) ? title.value : title
if (!titleValue) {
return
}
document.title = titleValue + ' - ' + NAME
}
if (isRef(title)) {
watchEffect(updatePageTitle)
}
// 在组件挂载时更新网页标题
onMounted(() => {
updatePageTitle()
})
// 在组件卸载时恢复原始网页标题
onBeforeUnmount(() => {
document.title = originalTitle.value
})
return {
updatePageTitle
}
}
export default usePageTitle
在页面上使用,如登录页面
<script setup>
import usePageTitle from '../hooks/usePageTitle'
usePageTitle('登录')
</script>
封装首页导航栏组件
添加文件 src/components/NavMenu.vue
<script setup>
import { watch, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router';
const router = useRouter();
const route = useRoute();
const activeMenu = ref('');
// 监听route.query.category的变化,实时更新activeMenu的值
watch(() => route.query.category, (val) => {
activeMenu.value = val;
});
function handleSelect(index) {
router.push({ path: '/', query: { category: index } });
}
</script>
<template>
<div class="menu">
<el-menu :default-active="activeMenu" mode="horizontal" @select="handleSelect">
<el-menu-item index="frontend">前端</el-menu-item>
<el-menu-item index="java">Java</el-menu-item>
<el-menu-item index="python">Python</el-menu-item>
<el-menu-item index="mini-program">小程序</el-menu-item>
</el-menu>
</div>
</template>
<style scoped>
.menu {
margin-bottom: 20px;
}
</style>
封装首页搜索组件
添加文件 src/components/SearchInput.vue
<script setup>
import { useRouter, useRoute } from 'vue-router';
import { watch, ref } from 'vue';
const router = useRouter();
const route = useRoute();
const keyword = ref(route.query.keyword || '');
watch(() => route.query.keyword, (value) => {
keyword.value = value || '';
});
function handleSearch() {
const path = route.path;
const newQuery = { ...route.query, keyword: keyword.value };
router.push({ path, query: newQuery });
}
function handleClear() {
keyword.value = '';
const path = route.path;
const newQuery = { ...route.query, keyword: '' };
router.push({ path, query: newQuery });
}
</script>
<template>
<div class="search">
<el-input v-model="keyword" placeholder="请输入关键字" @keyup.enter.native="handleSearch" @clear="handleClear" clearable>
<el-button slot="append" icon="el-icon-search" @click="handleSearch"></el-button>
</el-input>
</div>
</template>
<style scoped>
.search {
width: 240px;
}
</style>
用户注册页
form表单提交数据 src/views/RegisterView.vue
<script setup>
import { useRouter } from 'vue-router';
import { ref, reactive } from 'vue';
import { register } from '../api/userApi';
import usePageTitle from '@/hooks/usePageTitle'
usePageTitle('注册')
const router = useRouter();
const formRef = ref()
const registerForm = reactive({
username: '',
password: '',
confirmPassword: '',
nickname: '',
});
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ pattern: /^[A-Za-z0-9_]+$/, message: '用户名只能包含字母、数字和下划线' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 5, max: 10, message: '密码长度在5到10个字符之间', trigger: 'blur' },
],
confirmPassword: [
{ required: true, message: '请输入确认密码', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value !== registerForm.password) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
},
trigger: 'blur',
},
],
};
function handleRegister(formEl) {
// 表单验证
if (!formEl) return
formEl.validate(async (valid) => {
if (valid) {
// 执行注册逻辑
await register(registerForm);
router.push('/login');
} else {
console.log('表单校验失败');
return false;
}
});
}
</script>
<template>
<div class="register">
<div class="register-container">
<h2 class="register-title">新用户注册</h2>
<el-form :model="registerForm" :rules="rules" ref="formRef" label-position="top" class="register-form">
<el-form-item label="用户名" prop="username">
<el-input v-model="registerForm.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="registerForm.password"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input type="password" v-model="registerForm.confirmPassword"></el-input>
</el-form-item>
<el-form-item label="昵称">
<el-input v-model="registerForm.nickname"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleRegister(formRef)">注册</el-button>
<router-link to="/login">已有账号?去登录</router-link>
</el-form-item>
</el-form>
</div>
</div>
</template>
<style scoped>
.register {
display: flex;
align-items: center;
justify-content: center;
height: 80vh;
}
.register-container {
width: 400px;
padding: 20px;
background-color: #f6f6f6;
border-radius: 4px;
}
.register-title {
text-align: center;
margin-bottom: 20px;
}
.register-form {
margin-top: 20px;
}
</style>
用户登录页
在用户登录后,存储token到本地 src/views/LoginView.vue
<script setup>
import { ref, reactive } from 'vue';
import { useRouter } from 'vue-router';
import userApi from '../api/userApi';
import usePageTitle from '@/hooks/usePageTitle'
usePageTitle('登录')
const router = useRouter();
const formRef = ref()
const loginForm = reactive({
username: '',
password: '',
});
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ pattern: /^[A-Za-z0-9_]+$/, message: '用户名只能包含字母、数字和下划线' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 5, max: 10, message: '密码长度在5到10个字符之间', trigger: 'blur' },
],
};
function handleLogin(formRef) {
// 表单验证
formRef.validate(async (valid) => {
if (!valid) {
console.log('表单校验失败');
return false;
}
// 执行登录逻辑
const { token } = await userApi.login(loginForm);
// 存储token
localStorage.setItem('token', token);
router.push('/');
});
}
</script>
<template>
<div class="login">
<div class="login-container">
<h2 class="login-title">用户登录</h2>
<el-form :model="loginForm" :rules="rules" ref="formRef" label-position="top" class="login-form">
<el-form-item label="用户名" prop="username">
<el-input v-model="loginForm.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="loginForm.password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin(formRef)">登录</el-button>
<router-link to="/register">没有账号?去注册</router-link>
</el-form-item>
</el-form>
</div>
</div>
</template>
<style scoped>
.login {
display: flex;
align-items: center;
justify-content: center;
height: 80vh;
}
.login-container {
width: 400px;
padding: 20px;
background-color: #f6f6f6;
border-radius: 4px;
}
.login-title {
text-align: center;
margin-bottom: 20px;
}
.login-form {
margin-top: 20px;
}
</style>
状态管理Pinia
Pinia使用例子
src/stores/counter.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
在页面中使用 counter
<script setup>
import { useCounterStore } from '../stores/counter'
const counterStore = useCounterStore()
// 解构会导致响应式丢失
// const { count, doubleCount, increment } = useCounterStore()
</script>
<template>
<main>
<h1>{{ counterStore.count }}</h1>
<h1>{{ counterStore.doubleCount }}</h1>
<button @click="counterStore.increment">加1</button>
</main>
</template>
<style scoped></style>
使用Pinia管理用户状态
添加文件 src/stores/user.js
import { reactive } from 'vue'
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
const userInfo = reactive({
username: '',
nickname: ''
})
function setUserInfo({ username, nickname }) {
userInfo.username = username
userInfo.nickname = nickname
}
function clearUserInfo() {
userInfo.username = ''
userInfo.nickname = ''
}
return { userInfo, setUserInfo, clearUserInfo }
})
在首页 src/App.vue
中获取用户信息
<script setup>
import { RouterLink, RouterView } from 'vue-router'
import NavMenu from './components/NavMenu.vue';
import SearchInput from './components/SearchInput.vue'
import UserInfo from './components/UserInfo.vue';
import { getUserInfo } from './api/userApi';
import { useUserStore } from './stores/user';
const userStore = useUserStore()
getUserInfo().then((res) => {
console.log(res)
userStore.setUserInfo(res)
})
</script>
<template>
<div class="container">
<!-- 顶部导航栏 -->
<div class="navbar">
<div class="navbar-left">
<div class="logo">
<RouterLink to="/">TechBlog</RouterLink>
</div>
<NavMenu class="nav-menu" />
<SearchInput />
</div>
<div class="navbar-right">
<UserInfo />
</div>
</div>
<!-- 主体部分 -->
<div class="main">
<div class="content">
<router-view></router-view>
</div>
</div>
</div>
</template>
<style scoped>
.container {
width: 100%;
}
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0px;
border-bottom: solid 1px var(--el-menu-border-color);
}
.navbar-left {
display: flex;
align-items: center;
}
.nav-menu {
border-bottom: none;
margin-left: 24px;
margin-bottom: 0px;
}
.logo {
font-size: 18px;
font-weight: bold;
}
.logo a {
text-decoration: none;
}
.navbar-right {
display: flex;
align-items: center;
}
.main {
width: 100%;
max-width: 1000px;
margin: 0 auto;
padding: 24px;
}
.content {
padding: 24px;
}
</style>
封装用户菜单组件
添加文件 src/components/UserInfo.vue
<script setup>
import { RouterLink, useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user';
const { userInfo, clearUserInfo } = useUserStore()
const router = useRouter();
const goCreateEditBlog = () => {
router.push('/create')
}
const goMyBlogs = () => {
router.push('/myBlog')
}
const logout = () => {
clearUserInfo()
localStorage.removeItem('token')
router.push('/login')
}
</script>
<template>
<div>
<!-- 如果userInfo.userinfo 存在则显示userInfo.userinfo,否则显示登录链接-->
<div class="dropdown" v-if="userInfo.username">
<el-dropdown>
<span class="el-dropdown-link">
{{userInfo.nickname || userInfo.username}}
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="goCreateEditBlog"><el-icon><Edit /></el-icon>创建博客</el-dropdown-item>
<el-dropdown-item @click="goMyBlogs"><el-icon><Files /></el-icon>我的博客</el-dropdown-item>
<el-dropdown-item @click="logout"><el-icon><SwitchButton /></el-icon>注销</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<RouterLink v-else to="/login">登录</RouterLink>
</div>
</template>
添加hooks函数,判断登录状态
添加文件 src/hooks/useNavToHome.js
import { watch } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
function useNavToHome() {
const router = useRouter();
const { userInfo } = useUserStore()
// 监听 userInfo.username ,如果有值,说明用户已经登录,跳转到首页
watch(() => userInfo.username, (val) => {
if (val) {
router.push('/');
}
}, { immediate: true });
}
export default useNavToHome
在注册、登录页面中添加,如果已登录则跳转到首页
import useNavToHome from '@/hooks/useNavToHome'
useNavToHome()
首页列表分页展示博客信息
添加卡片式组件,展示博客关键信息 src/components/BlogCard.vue
<script setup>
import { formatDate } from '@/utils/date';
defineProps({
blog: {
type: Object,
required: true,
},
});
</script>
<template>
<div class="blog-card">
<div class="top">
<div class="author-category-time">
<el-icon>
<User />
</el-icon>{{ blog.author }}
<i class="el-icon-folder"></i>{{ blog.category }}
<i class="el-icon-time"></i>{{ formatDate(blog.updatedAt) }}
</div>
</div>
<div class="middle">
<router-link :to="`/blog/${ blog.id }`">
<h4 class="title">{{ blog.title }}</h4>
</router-link>
<p class="description">{{ blog.description }}</p>
</div>
<div class="bottom">
<div class="stats">
<el-icon>
<Pointer />
</el-icon>{{ blog.likes }}
<el-icon>
<Star />
</el-icon>{{ blog.favorites }}
<el-icon>
<ChatLineSquare />
</el-icon>{{ blog.comments }}
</div>
</div>
</div>
</template>
<style scoped>
.blog-card {
background-color: #ffffff;
border-radius: 10px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
transition: background-color 0.3s ease;
}
.blog-card:hover {
background-color: #f5f5f5;
}
.top {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.author-category-time i {
margin-right: 5px;
}
.middle {
margin-bottom: 10px;
}
.title {
font-size: 20px;
}
.description {
color: #666666;
}
.bottom {
display: flex;
align-items: center;
}
.stats i {
margin-right: 5px;
}</style>
列表分页显示 src/views/HomeView.vue
<script setup>
import { ref, watchEffect } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getBlogList } from '@/api/blogApi'
import usePageTitle from '@/hooks/usePageTitle'
import BlogCard from '@/components/BlogCard.vue'
usePageTitle('首页')
// // 定义 list total
const list = ref([])
const total = ref(0)
const currentPage = ref(1)
const pageSizeRef = ref(10)
const $route = useRoute()
const $router = useRouter()
// 使用 watchEffect监听url参数page、pageSize、category、keyword的变化,然后调用获取博客列表的接口
watchEffect(async () => {
const query = $route.query
currentPage.value = Number(query.page) || 1
pageSizeRef.value = Number(query.pageSize) || 10
const page = Number(query.page) || 1
const pageSize = Number(query.pageSize) || 10
const category = query.category || ''
const keyword = query.keyword || ''
const res = await getBlogList({ page, pageSize, category, keyword })
list.value = res.list
total.value = res.total
})
const handlePageChange = (page) => {
const path = $route.path
const query = { ...$route.query, page }
$router.push({ path, query })
}
</script>
<template>
<main>
<div class="blog-container" v-for="item in list" :key="item.id">
<BlogCard :blog="item" />
</div>
<div class="pagination">
<el-pagination v-model="currentPage" :total="total" :page-size="pageSizeRef" layout="prev, pager, next"
:pager-count="7"
@current-change="handlePageChange"></el-pagination>
</div>
</main>
</template>
<style scoped>
.blog-container {
margin-bottom: 16px;
}
.pagination {
display: flex;
justify-content: center;
margin-top: 20px;
}
</style>
博客详情页
博客内容存储为marddown格式的文本
安装markdown工具 markdown-it
npm install markdown-it --save
博客详情页 src/view/BlogDetailView.vue
<script setup>
import { reactive, onMounted, toRef, toRaw } from 'vue'
import { useRoute } from 'vue-router'
import { getBlogById } from '../api/blogApi'
import usePageTitle from '@/hooks/usePageTitle'
import { formatDate } from '@/utils/date'
import MarkdownIt from 'markdown-it'
const md = MarkdownIt()
const $route = useRoute()
const blogId = $route.params.id || ''
const blogInfo = reactive({})
// onMounted 获取博客详情
onMounted(async () => {
const res = await getBlogById(blogId)
console.log(res)
blogInfo.title = res.title
blogInfo.content = res.content
blogInfo.createdAt = res.createdAt
blogInfo.updatedAt = res.updatedAt
blogInfo.category = res.category
blogInfo.author = res.author
blogInfo.id = res.id
blogInfo.likes = res.likes
blogInfo.comments = res.comments
blogInfo.favorites = res.favorites
blogInfo.isLiked = res.isLiked
blogInfo.isFavorited = res.isFavorited
})
usePageTitle(toRef(blogInfo, 'title'))
</script>
<template>
<main>
<!-- 显示 title author createdAt content -->
<div class="blog-detail">
<h2>{{ blogInfo.title }}</h2>
<div class="blog-info">
<el-icon>
<User />
</el-icon><span class="author">{{ blogInfo.author }}</span>
<span class="createdAt">{{ formatDate(blogInfo.createdAt) }}</span>
<span class="category">{{ blogInfo.category }}</span>
</div>
<div class="content" v-html="md.render(toRaw(blogInfo.content) || '')"></div>
</div>
</main>
</template>
<style scoped>
.blog-detail {
margin-bottom: 16px;
}
.blog-detail h2 {
font-size: 24px;
font-weight: bold;
margin-bottom: 16px;
}
.blog-detail .blog-info {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
}
.blog-detail .blog-info .author {
color: #999;
}
.content {
line-height: 1.8;
font-size: 16px;
color: #333;
}
</style>
封装分页组件
添加文件 src/components/ListPage.vue
<script setup>
import { useRoute, useRouter } from 'vue-router'
const $route = useRoute()
const $router = useRouter()
defineProps({
total: {
type: Number,
required: true
},
currentPage: {
type: Number,
required: true
},
pageSizeRef: {
type: Number,
required: true
}
})
const handlePageChange = (newPage) => {
$router.push({
query: {
...$route.query,
page: newPage
}
})
};
</script>
<template>
<div class="pagination">
<el-pagination
:current-page="currentPage"
:page-size="pageSizeRef"
:total="total"
layout="prev, pager, next"
@current-change="handlePageChange"
></el-pagination>
</div>
</template>
<style scoped>
.pagination {
display: flex;
justify-content: center;
margin-top: 20px;
}
</style>
我的博客页面
使用table表格展示列表数据 src/views/MyBlogView.vue
<script setup>
import { useRouter } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import usePageTitle from '@/hooks/usePageTitle'
import useNavToLogin from '@/hooks/useNavToLogin'
import useGetBlogList from '@/hooks/useGetBlogList'
import ListPage from '@/components/ListPage.vue';
import { deleteBlog } from '@/api/blogApi';
const $router = useRouter()
useNavToLogin() // 未登录时跳转到登录页
usePageTitle('我的博客')
const { list, total, currentPage, pageSizeRef, getBlogListFn } = useGetBlogList({ my: true })
// 编辑博客,跳转到编辑页
const handleEdit = (blog) => {
$router.push(`/blog/${blog.id}/edit`)
}
// 删除博客
const handleDelete = async (blog) => {
// confirm 验证
const confirm = await ElMessageBox.confirm('此操作将永久删除该博客, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
if (confirm !== 'confirm') return
try {
await deleteBlog(blog.id)
getBlogListFn() // 删除成功后重新获取博客列表
} catch (error) {
console.log(error)
}
}
</script>
<template>
<div class="title-container">
<h2>我的博客</h2>
<router-link to="/create">
<el-button type="primary">创建博客</el-button>
</router-link>
</div>
<!-- 用 table 显示博客列表,包括 title category likes favorites comments createdAt ,再加两个操作按钮"编辑" "删除" -->
<el-table :data="list" border style="width: 100%">
<!-- 标题列,点击标题链接到博客详情页 -->
<el-table-column label="标题">
<template #default="{ row }">
<router-link :to="`/blog/${row.id}`">{{ row.title }}</router-link>
</template>
</el-table-column>
<el-table-column prop="category" label="分类"></el-table-column>
<el-table-column prop="likes" label="点赞数"></el-table-column>
<el-table-column prop="favorites" label="收藏数"></el-table-column>
<el-table-column prop="comments" label="评论数"></el-table-column>
<el-table-column prop="createdAt" label="创建时间"></el-table-column>
<el-table-column label="操作">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!--分页组件-->
<ListPage :total="total" :currentPage="currentPage" :pageSizeRef="pageSizeRef" />
</template>
<style scoped>
.title-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
</style>
新建、编辑博客页面
添加文件 src/views/CreateEditBlogView.vue
<script setup>
import { onMounted, reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import usePageTitle from '@/hooks/usePageTitle'
import useNavToLogin from '@/hooks/useNavToLogin'
import { getBlogById, createBlog, editBlog } from '../api/blogApi';
useNavToLogin() // 未登录时跳转到登录页
const $route = useRoute()
const $router = useRouter()
const id = $route.params.id
// 定义 title ,根据 id 判断 title
const title = id ? '编辑博客' : '创建博客'
usePageTitle(title)
// onMounted 时候,如果有 id ,则获取博客详情
const blogDetail = reactive({})
onMounted(async () => {
if (!id) return
const data = await getBlogById(id)
blogDetail.title = data.title
blogDetail.content = data.content
blogDetail.category = data.category
})
// 表单验证
const validateForm = () => {
if (!blogDetail.title) {
alert('请输入标题');
return false;
}
if (!blogDetail.content) {
alert('请输入内容');
return false;
}
if (!blogDetail.category) {
alert('请选择类型');
return false;
}
return true;
};
// 确认提交
const confirmSubmit = async () => {
const confirm = await ElMessageBox.confirm('提交数据?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
return confirm === 'confirm';
};
// 提交
const submit = async () => {
if (!validateForm()) return;
if (!await confirmSubmit()) return;
const data = {
title: blogDetail.title,
content: blogDetail.content,
category: blogDetail.category
}
if (id) {
await editBlog(id, data)
} else {
await createBlog(data)
}
$router.push('/myBlog')
}
</script>
<template>
<div class="top-container">
<div class="top-left">
<h2>{{ title }}</h2>
</div>
<div class="top-right">
<el-button type="primary" @click="submit">提交</el-button>
</div>
</div>
<div class="title-container">
<div class="title-left">
<span class="label">标题</span>
<el-input v-model="blogDetail.title" placeholder="请输入标题" style="width: 500px;"></el-input>
</div>
<div class="title-right">
<span class="label">类型</span>
<el-select v-model="blogDetail.category" placeholder="请选择类型">
<el-option label="前端" value="前端"></el-option>
<el-option label="Java" value="Java"></el-option>
<el-option label="Python" value="Python"></el-option>
<el-option label="小程序" value="小程序"></el-option>
</el-select>
</div>
</div>
<div class="content-container">
<el-input type="textarea" :rows="20" v-model="blogDetail.content" placeholder="请输入内容"
style="width: 100%;"></el-input>
</div>
</template>
<style scoped>
.top-container {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
/* border: 1px solid #ccc; */
}
.top-left {
flex-grow: 1;
}
.top-right {
margin-left: 10px;
}
.title-container {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
/* border: 1px solid #ccc; */
}
.title-left,
.title-right {
display: flex;
align-items: center;
}
.title-right {
margin-left: 20px;
}
.label {
display: inline-block;
width: 45px;
}
</style>
性能优化--打包时拆分第三方依赖
打包项目
npm run build
可以查看打包后每个文件的大小
安装 Import Cost
插件,可以在每个vue文件中,查看第三方依赖库的文件大小
Vue、Element-Plus 可以拆分出来,减小首页index文件的大小
修改配置文件 vite.config.js
build: {
rollupOptions: {
output: {
manualChunks: {
// 将 Vue 和 Element Plus 单独打包为一个公共块
vue: ['vue'],
'element-plus': ['element-plus'],
'markdown-it': ['markdown-it']
}
}
}
}