5.1 使用vite + vue搭建
win + r 打开终端
切换到你想要搭建的盘
npm init vite@latest
跟着以下步骤取名即可
cd fullStackBlog
npm install
npm run dev
默认在 http://localhost:5173/ 下启动了
5.2 用vscode打开项目并安装需要的插件
1、删除多余的 HelloWorld.vue 文件
2、安装需要的插件
网络请求我直接用fetch了,你需要用axios的话就执行以下命令安装,使用也很简单
npm i axios -S
安装Element-Plus并引入到入口文件 main.js (这里使用了全局引入,按需引入的参考官网,很简单)
安装element-plus/icons-vue图标
npm install @element-plus/icons-vue
全局引入
npm install element-plus --save
在main.js中引入:
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
app.use(ElementPlus)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.mount('#app')
**按需引入需要装两个插件,然后在vite.config.js中配置(打开注释部分即可):
npm install -D unplugin-vue-components unplugin-auto-import unplugin-icons
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// import AutoImport from 'unplugin-auto-import/vite'
// import Components from 'unplugin-vue-components/vite'
// import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// import Icons from 'unplugin-icons/vite'
// import IconsResolver from 'unplugin-icons/resolver'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
// AutoImport({
// resolvers: [
// ElementPlusResolver(),
// IconsResolver({
// prefix: 'Icon',
// })
// ],
// }),
// Components({
// resolvers: [
// ElementPlusResolver(),
// IconsResolver({
// enabledCollections: ['ep'],
// }),
// ],
// }),
// Icons({
// autoInstall: true,
// }),
],
})
安装tailwindcss并配置
执行以下命令安装tailwindcss和相应的插件
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
会生成 tailwind.config.js 和 postcss.config.js 文件即可
然后在assets文件夹下创建一个 tailwind.css 文件(名称可以自定义) 写上以下代码,并引入到入口文件main.js
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
// postcss.config.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
// tailwind.css文件
@tailwind base;
@tailwind components;
@tailwind utilities;
// main.js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import './assets/css/tailwind.css'
import './style.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
3、在App.vue中验证element 与 tailwind 是否生效
// App.vue
<template>
<div>
<h4 class="text-3xl font-blod underline">demo</h4>
<el-button class="mt-24" type="primary">button</el-button>
</div>
</template>
<script setup>
</script>
<style scoped>
</style>
验证没问题!
5.3 页面布局
安装vue-router路由
npm install vue-router@4
在src目录下新建router文件夹,新建index.js路由文件
// router/index.js
import { createRouter, createWebHistory } from "vue-router";
const route = [
{
path: '/',
component: () => import('../views/blog/List.vue')
},
{
path: '/add',
component: () => import('../views/blog/Add.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes: [...route]
});
export default router;
在src下新建views文件夹,在views下新建blog文件夹,在blog下新建List.vue 和 Add.vue 文件
// List.vue
<template>
<div>list</div>
</template>
<script setup>
</script>
<style scoped>
</style>
// Add.vue
<template>
<div>add</div>
</template>
<script setup>
</script>
<style scoped>
</style>
在main.js中引入路由并挂载
// main.js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import './assets/css/tailwind.css'
import './style.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.use(ElementPlus)
app.mount('#app')
在components下新建Header.vue Main.vue Footer.vue Nav.vue 并添加如下代码:
// Header.vue
<template>
<div>
<el-header class="h-16 p-4 header-wrapper">
<el-row>
<el-col :span="12">
<button class="button" data-text="Awesome" @click="goToHome">
<span class="actual-text"> blog </span>
<span aria-hidden="true" class="hover-text"> blog </span>
</button>
</el-col>
<el-col :span="12" class="flex justify-end">
<Nav />
</el-col>
</el-row>
</el-header>
</div>
</template>
<script setup>
import Nav from './Nav.vue'
</script>
<style scoped>
.header-wrapper{
line-height: 64px;
}
</style>
<style scoped>
.button {
margin: 0;
height: auto;
background: transparent;
padding: 0;
border: none;
cursor: pointer;
}
.button {
--border-right: 6px;
--text-stroke-color: #1da1f2;
--animation-color: #1da1f2;
--fs-size: 2em;
letter-spacing: 3px;
text-decoration: none;
font-size: var(--fs-size);
font-family: "Arial";
position: relative;
text-transform: uppercase;
color: transparent;
-webkit-text-stroke: 1px var(--text-stroke-color);
}
.hover-text {
position: absolute;
box-sizing: border-box;
content: attr(data-text);
color: var(--animation-color);
width: 0%;
inset: 0;
border-right: var(--border-right) solid var(--animation-color);
overflow: hidden;
transition: 0.5s;
-webkit-text-stroke: 1px var(--animation-color);
}
.button:hover .hover-text {
width: 100%;
filter: drop-shadow(0 0 23px var(--animation-color))
}
</style>
// Main.vue
<template>
<div>
<el-main class="flex justify-center items-center container">
<RouterView />
</el-main>
</div>
</template>
<script setup>
</script>
<style scoped>
.container {
min-width: 100%;
height: calc(100vh - 128px);
}
</style>
// Footer.vue
<template>
<div>
<el-footer class="h-16 flex justify-center items-center bg-black text-white">Footer</el-footer>
</div>
</template>
<script setup>
</script>
<style scoped></style>
// Nav.vue
<template>
<div class="button-container">
<button class="button" @click="goHome">
List
</button>
<button class="button" @click="goAdd">
Add
</button>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const goHome = () => {
router.push({ path: '/' })
}
const goAdd = () => {
router.push({ path: '/add' })
}
</script>
<style scoped>
.button-container {
display: flex;
background-color: rgba(0, 73, 144);
width: 250px;
height: 40px;
align-items: center;
justify-content: space-around;
border-radius: 10px;
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px,
rgba(0, 73, 144, 0.5) 5px 10px 15px;
transition: all 0.5s;
}
.button-container:hover {
width: 300px;
transition: all 0.5s;
}
.button {
outline: 0 !important;
border: 0 !important;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: transparent;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
transition: all ease-in-out 0.3s;
cursor: pointer;
}
.button:hover {
transform: translateY(-3px);
}
.icon {
font-size: 20px;
}
</style>
// App.vue改造为以下结构
<template>
<div>
<Header></Header>
<Main></Main>
<Footer></Footer>
</div>
</template>
<script setup>
import Header from './components/Header.vue'
import Main from './components/Main.vue'
import Footer from './components/Footer.vue'
</script>
<style scoped>
</style>
初步布局完成,页面如图:
5.4 新增博客页面 (前端部分)
页面使用了markdown语法,安装以下插件:
编写markdowm:mavon-editor
展示markdown:markdown-it、highlight.js
vue2下安装mavon-editor,vue3下安装mavon-editor@next
npm install mavon-editor@next markdown-it highlight.js --save
// Add.vue
四个字段:
标题:title
作者: auth
文档:mdoc
创建时间:createtime
// markdowm 暂时对图片没做处理
<template>
<div class="add-wrapper">
<div class="top-anonymous">
<el-input v-model="title" placeholder="标题(选填)"></el-input>
<el-input class="mt-4" v-model="auth" placeholder="作者(选填)"></el-input>
</div>
<div class="bottom-anonymous">
<mavon-editor class="h-full" :toolbars="markdownOption" v-model="mdoc" />
</div>
<button class="mt-8 button_submit" @click="save">
<div class="button_submit__int">
<span class="button_submit__span">Submit</span>
</div>
</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ElNotification } from 'element-plus'
import { useRouter } from 'vue-router'
import { dayjs } from 'element-plus'
import { mavonEditor } from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
const router = useRouter()
let mdoc = ref('')
let markdownOption = ref({
bold: true, // 粗体
italic: true, // 斜体
header: true, // 标题
underline: true, // 下划线
strikethrough: true, // 中划线
mark: true, // 标记
superscript: true, // 上角标
subscript: true, // 下角标
quote: true, // 引用
ol: true, // 有序列表
ul: true, // 无序列表
link: true, // 链接
imagelink: true, // 图片链接
code: true, // code
table: true, // 表格
fullscreen: false, // 全屏编辑
readmodel: false, // 沉浸式阅读
htmlcode: true, // 展示html源码
help: true, // 帮助
undo: true, // 上一步
redo: true, // 下一步
trash: true, // 清空
save: true, // 保存(触发events中的save事件)
navigation: true, // 导航目录
alignleft: true, // 左对齐
aligncenter: true, // 居中
alignright: true, // 右对齐
subfield: true, // 单双栏模式
preview: true, // 预览
})
let title = ref('')
let auth = ref('')
const save = async () => {
if (!mdoc.value) {
ElNotification({
message: '请录入内容~~',
type: 'error'
})
return
}
const params = {
title: title.value || `标题--${dayjs().format('YYYY-MM-DD HH:mm:ss')}`,
auth: auth.value || '恋爱单排选手',
mdoc: mdoc.value,
createtime: dayjs().format('YYYY-MM-DD HH:mm:ss')
}
const response = await request(params)
console.log(response)
if (!response.ok) {
ElNotification({
message: response.message,
type: 'error'
})
return
}
ElNotification({
message: '写入成功~~',
type: 'success'
})
router.push({ path: '/' })
}
const request = async (params) => {
const url = `${import.meta.env.VITE_API_BASE_URL}/api/add`
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
})
return response
}
</script>
<style scoped>
.add-wrapper {
width: 90%;
height: 96%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.top-anonymous {
width: 100%;
}
.bottom-anonymous {
margin-top: 32px;
width: 100%;
height: 100%;
}
</style>
<style scoped>
.button_submit {
background-image: linear-gradient(to right bottom, #e300ff, #ff00aa, #ff5956, #ffb900, #fffe00);
border: none;
font-size: 1.2em;
border-radius: 1.5em;
padding: 4px;
transition: border-top-left-radius 0.2s ease-in,
border-top-right-radius 0.2s ease-in 0.15s,
border-bottom-right-radius 0.2s ease-in 0.3s,
border-bottom-left-radius 0.2s ease-in 0.45s,
padding 0.2s ease-in;
position: relative;
}
.button_submit__int {
background-color: #212121;
color: white;
border-radius: 1.3em;
padding: 10px 40px;
transition: all 0.2s ease-in,
border-top-left-radius 0.2s ease-in,
border-top-right-radius 0.2s ease-in 0.15s,
border-bottom-right-radius 0.2s ease-in 0.3s,
border-bottom-left-radius 0.2s ease-in 0.45s,
padding 0.2s ease-in;
font-weight: 600;
z-index: -1;
box-shadow: -15px -10px 30px -5px rgba(225, 0, 255, 0.8),
15px -10px 30px -5px rgba(255, 0, 212, 0.8),
15px 10px 30px -5px rgba(255, 174, 0, 0.8),
-15px 10px 30px -5px rgba(255, 230, 0.8);
}
.button_submit:active .button_submit__int {
padding: 10px 30px;
}
.button_submit:hover {
border-radius: 0;
}
.button_submit:hover .button_submit__int {
border-radius: 0;
}
.button_submit:hover .button_submit__int {
box-shadow: -25px -10px 30px -5px rgba(225, 0, 255, 0.7),
25px -10px 30px -5px rgba(255, 0, 212, 0.7),
25px 10px 30px -5px rgba(255, 174, 0, 0.7),
-25px 10px 30px -5px rgba(255, 230, 0, 0.7);
}
</style>
5.4 列表页面展示(前端部分)
// List.vue 页面数据目前是伪造的,后面后端写完后直接调用
<template>
<div class="w-full h-full pt-10 box-border flex justify-between list-wrapper">
<div class="w-3/5 h-full mr-4 relative left-wrapper">
<div>
<input
class="w-full bg-[#004990] text-white font-mono ring-1 ring-zinc-400 focus:ring-2 focus:ring-blue-400 outline-none duration-300 placeholder:text-white placeholder:opacity-50 rounded-full px-4 py-1 shadow-md focus:shadow-lg focus:shadow-blue-400"
autocomplete="off"
placeholder="title..."
name="title"
type="text"
v-model="input"
@blur="search"
/>
</div>
<div class="mt-8 overflow-y-scroll blog-list">
<div v-for="item in blogList" :key="item">
<div class="text-3xl cursor-pointer" >
<span class="hover:text-[#1da1f2]">{{ item.title }}</span>
</div>
<div class="flex flex-row my-4">
<el-text class="basis-2/4" type="info">
<el-icon>
<Position />
</el-icon>
{{ item.auth }}
</el-text>
<el-text class="basis-2/4" type="info">
<el-icon>
<Compass />
</el-icon>
{{ item.createtime }}
</el-text>
</div>
<div class="mt-2 border rounded-md p-4">
<div v-html="md.render(item.mdoc)"></div>
</div>
<el-divider />
</div>
</div>
<el-pagination class="absolute bottom-0" :hide-on-single-page="isShowPage" background layout="prev, pager, next" :page-size="5"
:default-page-size="5" :current-page="currentPage" :total="totalNum" @current-change="currentChange" />
</div>
<div class="w-2/5 h-full right-wrapper">
<div class="h-24"></div>
<div class=""></div>
</div>
</div>
<div class="write-btn-wrapper" @click="goWrite">
<WriteBtn />
</div>
</template>
<script setup>
import { ref } from 'vue'
import WriteBtn from '../../components/WriteBtn.vue'
import markdownit from 'markdown-it'
import hljs from 'highlight.js/lib/core'
import 'highlight.js/styles/atom-one-dark.css'
import { useRouter } from 'vue-router'
const router = useRouter()
const goWrite = () => {
router.push({ path: '/add' })
}
let input = ref('')
let blogList = ref([
{
title: 'title1',
auth: 'auth1',
createtime: '2024-06-04',
mdoc: '## 132\n```language\nlet a = 1 + 1\n```\n'
},
{
title: 'title2',
auth: 'auth2',
createtime: '2024-06-04',
mdoc: '## 这是一个测试2'
},
{
title: 'title1',
auth: 'auth1',
createtime: '2024-06-04',
mdoc: '## 132\n```language\nlet a = 1 + 1\n```\n'
},
{
title: 'title1',
auth: 'auth1',
createtime: '2024-06-04',
mdoc: '## 132\n```language\nlet a = 1 + 1\n```\n'
},
{
title: 'title1',
auth: 'auth1',
createtime: '2024-06-04',
mdoc: '## 132\n```language\nlet a = 1 + 1\n```\n'
},
{
title: 'title1',
auth: 'auth1',
createtime: '2024-06-04',
mdoc: '## 132\n```language\nlet a = 1 + 1\n```\n'
},
])
let totalNum = ref(100)
let currentPage = ref(1)
// let isShowPage = computed(() => blogList.value.length <= 5)
let isShowPage = false
const currentChange = async (page) => {
currentPage.value = page
console.log(page, '=================')
requestData()
}
const md = markdownit({
html: true,
linkify: true,
typographer: true,
breaks: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return `<pre><code class="language-${lang} hljs">` +
hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
'</code></pre>';
} catch (__) { }
}
return '<pre><code class="language-none hljs">' + md.utils.escapeHtml(str) + '</code></pre>';
}
})
const search = () => {
requestData()
}
const requestData = async () => {
console.log(import.meta.env.VITE_API_BASE_URL, '============')
const url = `${import.meta.env.VITE_API_BASE_URL}/api/list`
console.log(input.value, '======input.value======')
const params = {
currentPage: currentPage.value - 1,
title: input.value.trim()
}
const queryString = new URLSearchParams(params).toString();
const requestUrl = `${url}?${queryString}`;
const response = await fetch(requestUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
if (!response.ok) {
throw new Error('Failed to fetch data')
}
const data = await response.json()
// console.log('Data from backend:', data)
const { data: resData, total } = data
blogList.value = resData
totalNum.value = total
}
</script>
<style scoped>
.write-btn-wrapper {
position: fixed;
top: 120px;
right: 40px;
}
.blog-list {
height: 90%;
}
</style>