全栈开发实战(二)——简易博客社区前端搭建教程(附源码)
全栈开发实战(二)——简易博客社区前端搭建
项目展示视频
项目Github地址
(一)项目准备
在开始我们的项目前,请确保你已安装好node.js及vue3.js,并配置好相应的编辑器,本项目所使用的编辑器为Visual Studio Code
1. 创建项目
在终端输入以下语句使用vite创建项目blog_client:
npm init vite@latest
cd进入输入以下语句安装必要的依赖:
npm install
输入以下语句运行项目:
npm run dev
2. 模块安装
本项目所使用的模块如下:
模块 | 说明 |
---|---|
axios | 基于promise的HTTP库,用于http请求 |
pinia | Vue的存储库,它允许您跨组件、页面共享状态 |
sass | CSS的开发工具,提供许多便利写法 |
vue-router | Vue.js官方的路由插件 |
naive-ui | Vue3的组件库 |
wangeditor | 富文本编辑器 |
从终端进入client文件夹,输入:
npm install axios
npm install pinia
npm install sass
Vue Router 安装文档
npm install vue-router@4
Naive UI安装文档
npm i -D naive-ui
npm i -D vfonts
npm i -D @vicons/ionicons5
wangEditor安装文档
npm install @wangeditor/editor-for-vue@next --save
安装上述模块
3. 修改全局格式文件(非必要)
将src/style.css修改为(当然,背景颜色可以自由选择):
body {background-color: #FCFAF7;margin: 0;padding: 0;
}
4. 新建文件夹存放图片(非必要)
在assets文件夹下新建文件夹image,该文件夹存放一些显示在页面上的图片
5. 引入基本的模块
在main.js中引入相关模块:
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import naive from "naive-ui"; // 引入ui框架
import { createDiscreteApi } from "naive-ui"; // 引入createDiscreteApi
import { createPinia } from "pinia"; // 引入pinia
import { router } from "./common/router"; // 引入路由
import axios from "axios"; // 引入axios
import { UserStore } from "./stores/UserStore" // 引入UserStoreaxios.defaults.baseURL = "http://localhost:8080"; // 服务端地址全局配置
const { message, notification, dialog } = createDiscreteApi(["message", "notification", "dialog"])const app = createApp(App);app.provide("axios", axios); // 将axios全局放入
app.provide("message", message)
app.provide("notification", notification)
app.provide("dialog", dialog)
app.provide("serverUrl", axios.defaults.baseURL)app.use(naive); // 引入ui框架
app.use(createPinia()); // 引入piniaapp.use(router); // 引入路由
app.mount("#app");
在src文件夹下新建文件夹common和views,在common文件夹下创建文件router.js,引入路由:
import { createRouter, createWebHashHistory } from "vue-router";let routes = [
]const router = createRouter({history: createWebHashHistory(),routes,
});export { router, routes }
修改App.vue如下:
<template><router-view ></router-view>
</template><script setup></script><style scoped></style>
(二)登录注册页
该页面用到的组件为表单Form表单 Form - Naive UI
在view文件夹下新建文件Register.vue,编写注册页,我们获取用户的用户名、手机号和密码并传给后端
<template><div class="background"><img src="../assets/image/rectangle1.png" class="rectangle1" /><img src="../assets/image/rectangle2.png" class="rectangle2" /><img src="../assets/image/rectangle3.png" class="rectangle3" /><img src="../assets/image/rectangle4.png" class="rectangle4" /><img src="../assets/image/person.png" class="person" /></div><div class="board"><div><div @click="toLogin" class="button2"><div style="position: absolute;left:22px;">登录</div></div><div class="button1"><div style="position:absolute;left:22px;">注册</div></div> </div><n-form ref="formRef" :rules="rules" :model="user"><n-form-item path="userName" style="position:absolute;left:70px;top:120px;width:350px;"><n-input v-model:value="user.userName" size="large" round placeholder="用户名"/> </n-form-item><n-form-item path="phoneNumber" style="position:absolute;left:70px;top:190px;width:350px;"><n-input v-model:value="user.phoneNumber" size="large" round placeholder="手机号"/> </n-form-item><n-form-item path="password" style="position:absolute;left:70px;top:260px;width:350px;"><n-input v-model:value="user.password" size="large" round type = "password" placeholder="密码"/> </n-form-item><n-form-item path="repeatPassword" style="position:absolute;left:70px;top:330px;width:350px;"><n-input v-model:value="user.repeatPassword" size="large" round type = "password" placeholder="重新输入密码"/> </n-form-item></n-form><div @click="submit" class="button3"><div style="left: auto;right: auto;text-align: center;">注册</div></div></div>
</template><script setup>
import {ref,reactive,inject} from 'vue'
import {useRouter, useRoute} from 'vue-router'
const router = useRouter()
const route = useRoute()const axios = inject("axios")
const message = inject("message")const formRef = ref(null)
const user = reactive({userName: "",phoneNumber: "",password:"",repeatPassword:"",
})function validatePasswordSame(rule, value) {return value == user.password;
}let rules = {userName: [{ required: true, message: "请输入用户名", trigger: "blur" },{ min: 3, max: 20, message: "用户名长度在 3 到 20 个字符", trigger: "blur"},],phoneNumber: [{ required: true, message: "请输入手机号", trigger: "blur" },{ min: 11, max: 11, message: "手机号为 11 位", trigger: "blur"},],password: [{ required: true, message: "请输入密码", trigger: "blur" },{ min: 6, max: 20, message: "密码长度在 6 到 20 个字符", trigger: "blur"}, ],repeatPassword: [{ required: true, message: "请重新输入密码", trigger: "blur" }, { validator: validatePasswordSame, message: "两次输入的密码不一致", trigger: "blur"},],
}function submit() {formRef.value?.validate((errors) => {if (errors) {message.error("注册失败")} else {register();} })
}const register = async() => {let res = await axios.post("/register", {userName: user.userName,phoneNumber: user.phoneNumber,password: user.password})console.log(res)if (res.data.code == 200) {message.success(res.data.msg)router.push({path: "login",query: {phoneNumber: user.phoneNumber,password: user.password}})} else {message.error(res.data.msg)}
}const toLogin = () => {router.push("/login")
}</script><style lang="scss" scoped>
.background {.rectangle1 {position: absolute;margin-left: -160px;top: -320px;z-index:-1;}.rectangle2 {position: absolute;left: 650px;top: 0px;z-index:-1;}.rectangle3 {position: absolute;left: 800px;top: -100px;z-index:-1;}.rectangle4 {position: absolute;left: 1100px;top: 450px;z-index:-1;}.person {position: absolute;left: 80px;top: 70px;z-index:-1;}
}
.person {position: absolute;left: 80px;top: 70px;z-index:-1;
}
.board {position: absolute;top: 95px;right: 235px;width: 500px;height: 550px;border-radius: 20px;box-shadow: 0px 20px 50px #D3D4D8;background-color: white;z-index: 0;.button1 {position: absolute;top: 75px;left: 150px;width: 80px;height: 40px;border-radius: 20px;background-color: #7B3DE0;line-height: 40px;font-size: 16px;color: white; cursor: default; }.button2 {position: absolute;top: 75px;left: 70px;width: 160px;height: 40px;border-radius: 20px;background-color: #F1EBFB; line-height: 40px;font-size: 16px;color: black;cursor: pointer;}.button3 {position: absolute;top: 430px;left: 70px;width: 350px;height: 50px;border-radius: 20px;background-color: #7B3DE0; line-height: 50px;font-size: 16px;color: white;cursor: pointer;}
}
</style>
由于用户登录完成后后端会返回一个token,我们需要将这个token保存起来,以便传给其他接口
在src文件夹下新建文件夹stores,在stores文件夹下新建文件UserStore.js,写入以下代码定义存储的内容
import { defineStore } from "pinia"; // 引入piniaexport const UserStore = defineStore("admin", {state: () => {return {token: "",};},actions: {},getters: {},
});
在view文件夹下新建文件Login.vue,编写登录页,我们获取用户的手机号和密码传给后端,登录成功后存储后端传过来的token
<template><div class="background"><img src="../assets/image/rectangle1.png" class="rectangle1" /><img src="../assets/image/rectangle2.png" class="rectangle2" /><img src="../assets/image/rectangle3.png" class="rectangle3" /><img src="../assets/image/rectangle4.png" class="rectangle4" /><img src="../assets/image/person.png" class="person" /></div><div class="board"><div><div @click="toRegister" class="button2"><div style="position: absolute;right:22px;">注册</div></div><div class="button1"><div style="position:absolute;left:22px;">登录</div></div> </div><n-form ref="formRef" :rules="rules" :model="user"><n-form-item path="phoneNumber" style="position:absolute;left:70px;top:150px;width:350px;"><n-input v-model:value="user.phoneNumber" size="large" round placeholder="手机号"/> </n-form-item><n-form-item path="password" style="position:absolute;left:70px;top:230px;width:350px;"><n-input v-model:value="user.password" size="large" round type = "password" placeholder="密码"/> </n-form-item></n-form><n-checkbox v-model:checked="user.rember" label="记住密码" style="position:absolute;left:70px;top:330px;"/><div @click="submit" class="button3"><div style="left: auto;right: auto;text-align: center;">登录</div></div></div>
</template><script setup>
import {ref,reactive,inject} from 'vue'
import {UserStore} from '../stores/UserStore'import {useRouter, useRoute} from 'vue-router'
const router = useRouter()
const route = useRoute()const axios = inject("axios")
const message = inject("message")
const userStore = UserStore()const formRef = ref(null)
const user = reactive({phoneNumber: localStorage.getItem("phoneNumber") || route.query.phoneNumber || "",password: localStorage.getItem("password") || route.query.password || "" ,rember: localStorage.getItem("rember") == 1 || false
})let rules = {phoneNumber: [{ required: true, message: "请输入手机号", trigger: "blur" },{ min: 11, max: 11, message: "手机号为 11 位", trigger: "blur"},],password: [{ required: true, message: "请输入密码", trigger: "blur" },{ min: 6, max: 20, message: "密码长度在 6 到 20 个字符", trigger: "blur"}, ]
}function submit() {formRef.value?.validate((errors) => {if (errors) {message.error("注册失败")} else {login();} })
}const login = async() => {let res = await axios.post("/login", {phoneNumber: user.phoneNumber,password: user.password})console.log(res)if (res.data.code == 200) {userStore.token = res.data.data.tokenif (user.rember) {localStorage.setItem("phoneNumber", user.phoneNumber)localStorage.setItem("password", user.password)localStorage.setItem("rember", user.rember? 1: 0)} else {localStorage.removeItem("phoneNumber")localStorage.removeItem("password")localStorage.setItem("rember", user.rember? 1: 0)} router.push("/")message.success(res.data.msg) } else {message.error(res.data.msg)}
}const toRegister = () => {router.push("/register")
}</script><style lang="scss" scoped>
.background {.rectangle1 {position: absolute;left: -160px;top: -320px;z-index:-1;}.rectangle2 {position: absolute;left: 650px;top: 0px;z-index:-1;}.rectangle3 {position: absolute;left: 800px;top: -100px;z-index:-1;}.rectangle4 {position: absolute;left: 1100px;top: 450px;z-index:-1;}.person {position: absolute;left: 80px;top: 70px;z-index:-1;}
}
.board {position: absolute;top: 95px;right: 235px;width: 500px;height: 550px;border-radius: 20px;box-shadow: 0px 20px 50px #D3D4D8;background-color: white;z-index: 0;.button1 {position: absolute;top: 75px;left: 70px;width: 80px;height: 40px;border-radius: 20px;background-color: #7B3DE0;line-height: 40px;font-size: 16px;color: white; cursor: default; }.button2 {position: absolute;top: 75px;left: 70px;width: 160px;height: 40px;border-radius: 20px;background-color: #F1EBFB; line-height: 40px;font-size: 16px;color: black;cursor: pointer;}.button3 {position: absolute;top: 400px;left: 70px;width: 350px;height: 50px;border-radius: 20px;background-color: #7B3DE0; line-height: 50px;font-size: 16px;color: white;cursor: pointer;}
}
</style>
编写完登录注册页后,将其加入路由,修改router.js
import { createRouter, createWebHashHistory } from "vue-router";let routes = [{ path: "/login", component: () => import("../views/Login.vue") },{ path: "/register", component: () => import("../views/Register.vue") },
]const router = createRouter({history: createWebHashHistory(),routes,
});export { router, routes }
修改App.vue
<template><router-view ></router-view>
</template><script setup></script><style scoped></style>
(三)顶栏组件
顶栏组件用到头像组件头像 Avatar - Naive UI和按钮组件按钮 Button - Naive UI
接下来我们编写一个顶栏组件,该组件可以跳转至主页、个人信息页、登录页以及发布文章页
我们先在view文件夹下新建文件MainFrame.vue、Myself.vue、Others.vue、Publish.vue、Update.vue、Detail.vue,写入空页面并添加进路由,便于跳转
import { createRouter, createWebHashHistory } from "vue-router";let routes = [{ path: "/login", component: () => import("../views/Login.vue") },{ path: "/register", component: () => import("../views/Register.vue") },{ path: "/", component: () => import("../views/MainFrame.vue") },{ path: "/publish", component: () => import("../views/Publish.vue") },{ path:"/myself", component: () => import("../views/Myself.vue") },{ path:"/others", component: () => import("../views/Others.vue") },{ path:"/detail", component: () => import("../views/Detail.vue") },{ path:"/update", component: () => import("../views/Update.vue") },
]const router = createRouter({history: createWebHashHistory(),routes,
});export { router, routes }
然后在components文件夹下新建文件TopBar.vue,编写顶栏,顶栏渲染时向后端接口/user获取头像,若用户已登录,将成功获取用户头像,否则可跳转至登录页
<template><div class="container"><div class="topbar"><div class="bigtitle" @click="toMain">首页</div><n-dropdown v-if="login" trigger="hover" :options="options" @select="handleSelect"><n-avatar @click="toHome" round size="medium" :src=user.avatarUrl style="position: absolute; right: 190px; top: 8px; cursor: pointer;"/></n-dropdown><div v-if="!login" class="smalltitle" @click="toLogin">登录/注册</div><div style="position: absolute; right: 50px; top: 8px"><n-button round color="#7B3DE0" @click="toPublish">发布文章</n-button></div> </div></div>
</template><script setup>
import {ref,reactive,inject, onMounted} from 'vue'import {useRouter, useRoute} from 'vue-router'
const router = useRouter()
const route = useRoute()const serverUrl = inject("serverUrl")
const axios = inject("axios")
const message = inject("message")const options = reactive([{label: "退出登录", key: "login"}])
const login = ref(false)
const user = reactive({avatarUrl: "",id: 0
})onMounted(() => {loadAvatar()
})const loadAvatar= async() => {let res = await axios.get("/user") console.log(res)if (res.data.code == 200) {user.avatarUrl = serverUrl + res.data.data.avataruser.id = res.data.data.idlogin.value = true}
} const toMain = () => {router.push("/")
}const toLogin = () => {router.push("/login")
}const toHome = () => {router.push({path: "/myself",query: {id: user.id}})
}const toPublish = () => {if (login.value == false) {message.warning("请先登录")} else {router.push("/publish") }
}const handleSelect = (key) => {router.push("/" + String(key))
}</script><style lang="scss" scoped>
.container {.topbar {position: sticky;top: 0;height: 50px;background: white;box-shadow: 0px 1px 5px #D3D4D8;.bigtitle {position: absolute;font-size: 20px;left: 50px;line-height: 50px;color: #7B3DE0;cursor: pointer;}.smalltitle {position: absolute;font-size: 16px;right: 175px;line-height: 50px;color: #7B3DE0;cursor: pointer; }}
}
</style>
修改main.js,添加拦截器传token,即每个页面都向后端传token,无论后端需不需要
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import naive from "naive-ui"; // 引入ui框架
import { createDiscreteApi } from "naive-ui"; // 引入createDiscreteApi
import { createPinia } from "pinia"; // 引入pinia
import { router } from "./common/router"; // 引入路由
import axios from "axios"; // 引入axios
import { UserStore } from "./stores/UserStore" // 引入UserStoreaxios.defaults.baseURL = "http://localhost:8080"; // 服务端地址全局配置
const { message, notification, dialog } = createDiscreteApi(["message", "notification", "dialog"])const app = createApp(App);app.provide("axios", axios); // 将axios全局放入
app.provide("message", message)
app.provide("notification", notification)
app.provide("dialog", dialog)
app.provide("serverUrl", axios.defaults.baseURL)app.use(naive); // 引入ui框架
app.use(createPinia()); // 引入piniaconst userStore = UserStore()
// 拦截器传token
axios.interceptors.request.use((config) => {config.headers.authorization = `Bearer ${userStore.token}`return config
})app.use(router); // 引入路由
app.mount("#app");
修改router.js添加路由
import { createRouter, createWebHashHistory } from "vue-router";let routes = [{ path: "/login", component: () => import("../views/Login.vue") },{ path: "/register", component: () => import("../views/Register.vue") },{ path: "/", component: () => import("../views/MainFrame.vue") },{ path: "/publish", component: () => import("../views/Publish.vue") },{ path:"/myself", component: () => import("../views/Myself.vue") },
]const router = createRouter({history: createWebHashHistory(),routes,
});export { router, routes }
(四)个人信息页
个人信息页用到的组件有卡片卡片 Card - Naive UI和模态框模态框 Modal - Naive UI,以及图标图标 Icon - Naive UI
用户点击头像可进入用户信息页,登录用户查看自身与他人的信息页渲染有所不同,自身的个人信息页有修改信息按键,而他人的个人信息页有关注按键
Myself.vue:
<template><div><div style="position: fixed; top: 0; height: 50px; width: 100%; z-index: 999; "><TopBar></TopBar></div><div class="card"><div style="position: absolute; left: 40px; bottom: 20px"><n-avatar round :size="120" :src=user.avatarUrl :bordered=true /></div><div style="position: absolute; top: 25px;left: 200px; font-size: 20px;">{{user.name}}</div><div style="position: absolute; top: 70px;left: 200px;"><text style="font-weight:bold; font-size: 20px;">{{user.number}}</text><text style="margin-left: 5px; font-size: 14px;">文章</text><text style="font-weight:bold; font-size: 20px; margin-left: 20px;">{{user.fans}}</text><text style="margin-left: 5px; font-size: 14px;">粉丝</text></div><n-dropdown trigger="hover" :options="options" @select="handleSelect"><n-button style="position: absolute; right: 40px; top: 25px;" round ghost color="#7B3DE0">修改资料</n-button></n-dropdown></div><n-modal v-model:show="showAvatarModal"><div style="width: 600px; height: 320px; background: white;"><n-card title="修改头像" :bordered="false"><n-uploadmultipledirectory-dnd:max="1"@before-upload="beforeUpload":custom-request="customRequest"><n-upload-dragger><div style="margin-bottom: 12px"><n-icon size="48" :depth="3"><archive-icon /></n-icon></div><n-text style="font-size: 16px">点击或者拖动图片到此处</n-text></n-upload-dragger></n-upload></n-card><div style="position: absolute; right: 90px; bottom: 20px;"><n-button type="default" @click="closeAvatarModal">取消</n-button></div><div style="position: absolute; right: 20px; bottom: 20px;"><n-button v-if="newAvatar" @click="modifyAvatar" type="primary">确认</n-button><n-button v-else type="primary" disabled>确认</n-button></div></div></n-modal><n-modal v-model:show="showNameModal"><div style="width: 440px; height: 185px; background: white;"><n-card title="修改用户名" :bordered="false"><div style="width:350px;"><n-input v-model:value="newName" size="large" round type="text" placeholder="请输入用户名" /></div></n-card><div style="position: absolute; right: 90px; bottom: 20px;"><n-button type="default" @click="closeNameModal">取消</n-button></div><div style="position: absolute; right: 20px; bottom: 20px;"><n-button type="primary" @click="modifyName">确认</n-button></div> </div></n-modal><div class="tabs"><n-card><n-tabs type="line" ><n-tab-pane name="articles" tab="我的文章"><div v-for="(article,index) in articles" style="margin-bottom:15px"><n-card v-if="article.head_image" @click="toDetail(article)" style="cursor: pointer;" hoverable ><n-image height="135" width="200" :src=serverUrl+article.head_image style="float: left" /><div style="position: absolute; left: 240px; width: 690px;"><text style="font-weight:bold; font-size: 20px;">{{article.title}}</text><p >{{article.content+"..."}}</p><div style="position: absolute; margin-top: 10px;">发布时间:{{article.created_at}}</div></div></n-card><n-card v-else @click="toDetail(article)" style="cursor: pointer;" hoverable ><div style="height: 140px; "><text style="font-weight:bold; font-size: 20px;">{{article.title}}</text><p >{{article.content+"..."}}</p><div style="position: absolute; margin-top: 10px;">发布时间:{{article.created_at}}</div> </div></n-card></div></n-tab-pane><n-tab-pane name="collects" tab="我的收藏"><div v-for="(col,index) in collects" style="margin-bottom:15px"><n-card v-if="col.head_image" @click="toDetail(col)" style="cursor: pointer;" hoverable ><n-image height="135" width="200" :src=serverUrl+col.head_image style="float: left" /><div style="position: absolute; left: 240px; width: 690px;"><text style="font-weight:bold; font-size: 20px;">{{col.title}}</text><p>{{col.content+"..."}}</p><div style="position: absolute; margin-top: 10px;">发布时间:{{col.created_at}}</div></div></n-card><n-card v-else style="cursor: pointer;" hoverable ><div style="height: 140px; "><text @click="toDetail(col)" style="font-weight:bold; font-size: 20px;">{{col.title}}</text><p @click="toDetail(col)" >{{col.content+"..."}}</p><div style="position: absolute; margin-top: 10px;">发布时间:{{col.created_at}}</div> </div></n-card></div></n-tab-pane><n-tab-pane name="following" tab="我的关注"><div v-for="(fol,index) in following" style="margin-bottom:15px"><n-card><n-avatar @click="toOtherUser(fol)" round size="large" :src=serverUrl+fol.avatar style="float: left; cursor: pointer;" /><text style="position: absolute; left: 90px; top: 25px; font-size: 20px;">{{fol.userName}}</text></n-card></div></n-tab-pane></n-tabs></n-card></div></div>
</template><script setup>
import {ref,reactive,inject, onMounted} from 'vue'
import TopBar from '../components/TopBar.vue'
import { ArchiveOutline as ArchiveIcon} from "@vicons/ionicons5"import {useRouter, useRoute} from 'vue-router'
const router = useRouter()
const route = useRoute()const serverUrl = inject("serverUrl")
const axios = inject("axios")
const message = inject("message")
const options = reactive([{label: "修改头像", key: "avatar"},{label: "修改用户名", key: "name"},
])
const user = reactive({self: false,avatarUrl: "",name: "",number: 0,fans: 0,id: 0
})
const newUrl = ref("")
const newAvatar = ref(false)
const newName = ref("")
const showAvatarModal = ref(false)
const showNameModal = ref(false)
const articles = ref([])
const collects = ref([])
const following = ref([])onMounted(() => {loadDetailedInfo()
})const loadDetailedInfo= async() => {let res = await axios.get("user/detailedInfo/" + route.query.id) console.log(res)if (res.data.code == 200) {user.self = res.data.data.selfuser.avatarUrl = serverUrl + res.data.data.avataruser.name = res.data.data.nameuser.number = res.data.data.articles.lengthuser.fans = res.data.data.fansuser.id = res.data.data.idarticles.value = res.data.data.articlescollects.value = res.data.data.collectsfollowing.value = res.data.data.followingnewName.value = user.name}
} const handleSelect = (key) => {if (String(key) == "avatar") {showAvatarModal.value = true} if (String(key) == "name") {showNameModal.value = true}
}const beforeUpload = async(data) => {if (data.file.file?.type !== "image/png") {message.error("只能上传png格式的图片")return false;}return true;
}const customRequest = async({file}) => {const formData = new FormData()formData.append('file', file.file)let res = await axios.post("/upload", formData)console.log(res)newUrl.value = res.data.data.filePathnewAvatar.value = true
}const modifyAvatar = async() => {let res = await axios.put("user/avatar/" + route.query.id,{avatar: newUrl.value,}) console.log(res) if (res.data.code == 200) {message.success(res.data.msg) showAvatarModal.value = false loadDetailedInfo()} else {message.error(res.data.msg) }
}const modifyName = async() => {let res = await axios.put("user/name/" + route.query.id,{userName: newName.value,}) console.log(res) if (res.data.code == 200) {message.success(res.data.msg) showNameModal.value = false loadDetailedInfo()} else {message.error(res.data.msg) }
}const closeAvatarModal = () => {showAvatarModal.value = false
}const closeNameModal = () => {showNameModal.value = false
}const toOtherUser = (fol) => {router.push({path: "/others",query: {id: fol.id}})
}const toDetail = (article) => {router.push({path: "/detail",query: {id: article.id}})
}</script><style lang="scss" scoped>
.card {position: absolute;top: 100px;left: 0;right: 0;margin: auto;width: 1000px;height: 130px;background: white; box-shadow: 0px 1px 3px #D3D4D8; border-radius: 5px;
}
.tabs {position: absolute;top: 250px;left: 0;right: 0;margin: auto;width: 1000px;height: auto;background: white; box-shadow: 0px 1px 3px #D3D4D8; border-radius: 5px;
}
.cardInfo {float: right;width: 80%;
}
</style>
Others.vue
<template><div><div style="position: fixed; top: 0; height: 50px; width: 100%; z-index: 999; "><TopBar></TopBar></div><div class="card"><div style="position: absolute; left: 40px; bottom: 20px"><n-avatar round :size="120" :src=user.avatarUrl :bordered=true /></div><div style="position: absolute; top: 25px;left: 200px; font-size: 20px;">{{user.name}}</div><div style="position: absolute; top: 70px;left: 200px;"><text style="font-weight:bold; font-size: 20px;">{{user.number}}</text><text style="margin-left: 5px; font-size: 14px;">文章</text><text style="font-weight:bold; font-size: 20px; margin-left: 20px;">{{user.fans}}</text><text style="margin-left: 5px; font-size: 14px;">粉丝</text></div><n-button v-if=!followed @click="newFollow" style="position: absolute; right: 40px; top: 25px; cursor: pointer;" round ghost color="#ED4557"><template #icon><n-icon><heart-outline /></n-icon></template>关注</n-button><n-button v-else @click="unFollow" style="position: absolute; right: 40px; top: 25px; cursor: pointer;" round ghost color="#ED4557"><template #icon><n-icon><heart /></n-icon></template>已关注</n-button></div><div class="tabs"><n-card><n-tabs type="line" ><n-tab-pane name="articles" tab="TA的文章"><div v-for="(article,index) in articles" style="margin-bottom:15px"><n-card v-if="article.head_image" @click="toDetail(article)" style="cursor: pointer;" hoverable ><n-image height="135" width="200" :src=serverUrl+article.head_image style="float: left" /><div style="position: absolute; left: 240px; width: 690px;"><text style="font-weight:bold; font-size: 20px;">{{article.title}}</text><p>{{article.content+"..."}}</p><div style="position: absolute; margin-top: 10px;">发布时间:{{article.created_at}}</div></div></n-card><n-card v-else style="cursor: pointer;" hoverable ><div style="height: 140px; "><text @click="toDetail(article)" style="font-weight:bold; font-size: 20px;">{{article.title}}</text><p @click="toDetail(article)" >{{article.content+"..."}}</p><div style="position: absolute; margin-top: 10px;">发布时间:{{article.created_at}}</div> </div></n-card></div></n-tab-pane><n-tab-pane name="collects" tab="TA的收藏"><div v-for="(col,index) in collects" style="margin-bottom:15px"><n-card v-if="col.head_image" @click="toDetail(col)" style="cursor: pointer;" hoverable ><n-image height="135" width="200" :src=serverUrl+col.head_image style="float: left" /><div style="position: absolute; left: 240px; width: 690px;"><text style="font-weight:bold; font-size: 20px;">{{col.title}}</text><p>{{col.content+"..."}}</p><div style="position: absolute; margin-top: 10px;">发布时间:{{col.created_at}}</div></div></n-card><n-card v-else style="cursor: pointer;" hoverable ><div style="height: 140px; "><text @click="toDetail(col)" style="font-weight:bold; font-size: 20px;">{{col.title}}</text><p @click="toDetail(col)" >{{article.content+"..."}}</p><div style="position: absolute; margin-top: 10px;">发布时间:{{col.created_at}}</div> </div></n-card></div></n-tab-pane><n-tab-pane name="following" tab="TA的关注"><div v-for="(fol,index) in following" style="margin-bottom:15px"><n-card><n-avatar @click="toOtherUser(fol)" round size="large" :src=serverUrl+fol.avatar style="float: left; cursor: pointer;" /><text style="position: absolute; left: 90px; top: 25px; font-size: 20px;">{{fol.userName}}</text></n-card></div></n-tab-pane></n-tabs></n-card></div></div>
</template><script setup>
import {ref,reactive,inject, onMounted} from 'vue'
import TopBar from '../components/TopBar.vue'
import {HeartOutline} from '@vicons/ionicons5'
import {Heart} from '@vicons/ionicons5'import {useRouter, useRoute} from 'vue-router'
const router = useRouter()
const route = useRoute()const serverUrl = inject("serverUrl")
const axios = inject("axios")
const message = inject("message")const user = reactive({self: false,avatarUrl: "",name: "",number: 0,fans: 0,id: 0,loginId: 0,
})
const articles = ref([])
const collects = ref([])
const following = ref([])
const followed = ref(false)
const index = ref(0)onMounted(() => {loadDetailedInfo()
})const loadDetailedInfo = async() => {let res1 = await axios.get("user/detailedInfo/" + route.query.id) console.log(res1)if (res1.data.code == 200) {user.self = res1.data.data.selfuser.avatarUrl = serverUrl + res1.data.data.avataruser.name = res1.data.data.nameuser.number = res1.data.data.articles.lengthuser.fans = res1.data.data.fansuser.id = res1.data.data.iduser.loginId = res1.data.data.loginIdarticles.value = res1.data.data.articlescollects.value = res1.data.data.collectsfollowing.value = res1.data.data.followinglet res2 = await axios.get("following/" + route.query.id) console.log(res2)if (res2.data.code == 200) {followed.value = res2.data.data.followedindex.value = res2.data.data.index}}
} const newFollow = async() => {let res1 = await axios.put("following/new/" + route.query.id)console.log(res1) if (res1.data.code == 200) {message.warning("已关注", {showIcon: false}) loadDetailedInfo() }
}const unFollow = async() => {let res1 = await axios.delete("following/" + index.value)console.log(res1) if (res1.data.code == 200) {message.warning("取消关注", {showIcon: false}) loadDetailedInfo() }
}const toOtherUser = (fol) => {console.log(fol.id, user.loginId)if (fol.id == user.loginId) {router.push({path: "/myself",query: {id: fol.id}}) } else {router.push({path: "/others",query: {id: fol.id}})}
}const toDetail = (article) => {router.push({path: "/detail",query: {id: article.id}})
}</script><style lang="scss" scoped>
.card {position: absolute;top: 100px;left: 0;right: 0;margin: auto;width: 1000px;height: 130px;background: white; box-shadow: 0px 1px 3px #D3D4D8; border-radius: 5px;
}
.tabs {position: absolute;top: 250px;left: 0;right: 0;margin: auto;width: 1000px;height: auto;background: white; box-shadow: 0px 1px 3px #D3D4D8; border-radius: 5px;
}
.cardInfo {float: right;width: 80%;
}
</style>
(五)主页
用户在主页的输入框文本输入 Input - Naive UI输入关键词,通过选择器弹出选择 Popselect - Naive UI选择分类,通过分页器分页 Pagination - Naive UI分页
MainFrame.vue
<template><div><div style="position: fixed; top: 0; height: 50px; width: 100%; z-index: 999; "><div class="card"><n-popselect @update:value="searchByCategory" v-model:value="selectedCategory" :options="categoryOptions" trigger="click"><n-button text style="position:absolute; left: 50px; top: 22px; font-size: 18px;">{{categoryName}}</n-button></n-popselect><n-input v-model:value="pageInfo.keyword" round placeholder="请输入关键字" style="position:absolute; left: 125px; top: 15px; width: 1000px; background-color: #F3F0F9;" /><n-button @click="loadArticles(0)" round color="#7B3DE0" style="position:absolute; left: 1150px; top: 15px;"><template #icon><n-icon><search /></n-icon></template>搜索</n-button></div> </div><div style="position: fixed; top: 0; height: 50px; width: 100%; z-index: 999; "><TopBar></TopBar></div><div class="tabs"><n-card><div v-for="(article,index) in articleList" style="margin-bottom:15px"><n-card v-if="article.head_image" @click="toDetail(article)" style="cursor: pointer;" hoverable ><n-image width="200" :src=serverUrl+article.head_image style="float: left" /><div style="position: absolute; left: 240px; width: 690px;"><text style="font-weight:bold; font-size: 20px;">{{article.title}}</text><p>{{article.content+"..."}}</p><div style="position: absolute; margin-top: 10px;">发布时间:{{article.created_at}}</div></div></n-card><n-card v-else style="cursor: pointer;" hoverable ><div style="height: 140px; "><text @click="toDetail(article)" style="font-weight:bold; font-size: 20px;">{{article.title}}</text><p @click="toDetail(article)" >{{article.content+"..."}}</p><div style="position: absolute; margin-top: 10px;">发布时间:{{article.created_at}}</div> </div></n-card></div><n-pagination @update:page="loadArticles" v-model:page="pageInfo.pageNum" :page-count="pageInfo.pageCount" /></n-card></div></div></template><script setup>
import TopBar from '../components/TopBar.vue'
import {ref,reactive,inject,onMounted,computed} from 'vue'
import {Search} from '@vicons/ionicons5'import {useRouter, useRoute} from 'vue-router'
const router = useRouter()
const route = useRoute()const serverUrl = inject("serverUrl")
const axios = inject("axios")
const message = inject("message")const selectedCategory = ref(0)
const categoryOptions = ref([])
const articleList = ref([])
const pageInfo = reactive({pageNum:1,pageSize:5,pageCount:0,count:0,keyword:"",categoryId:0
})onMounted(()=>{loadArticles()loadCategories()
})const loadArticles = async(pageNum = 0) =>{if (pageNum != 0){pageInfo.pageNum = pageNum;}let res = await axios.post(`/article/list?keyword=${pageInfo.keyword}&pageNum=${pageInfo.pageNum}&pageSize=${pageInfo.pageSize}&categoryId=${pageInfo.categoryId}`)console.log(res)if (res.data.code == 200) {articleList.value = res.data.data.article}pageInfo.count = res.data.data.count;pageInfo.pageCount = parseInt(pageInfo.count / pageInfo.pageSize) + (pageInfo.count % pageInfo.pageSize > 0 ? 1 : 0)console.log(pageInfo.pageNum, pageInfo.pageCount, pageInfo.count)
}const loadCategories = async() =>{let res = await axios.get("/category")console.log(res)categoryOptions.value = res.data.data.categories.map((item)=>{return {label:item.name,value:item.id}})
}const categoryName = computed(() => {let selectedOption = categoryOptions.value.find((option) => {return option.value == selectedCategory.value})console.log(selectedOption)return selectedOption ? selectedOption.label : ""
})const searchByCategory = (categoryId) => {pageInfo.categoryId = categoryIdpageInfo.pageNum = 1loadArticles()
}const toDetail = (article) => {router.push({path: "/detail",query: {id: article.id}})
}</script><style lang="scss" scoped>
.card {position: absolute;top: 50px;left: 0;right: 0;margin: auto;height: 60px;background: white; box-shadow: 0px 1px 3px #D3D4D8; border-radius: 5px;
}
.tabs {position: absolute;top: 150px;left: 0;right: 0;margin: auto;width: 1000px;height: auto;background: white; box-shadow: 0px 1px 3px #D3D4D8; border-radius: 5px;
}
</style>
(六)富文本编辑组件
https://www.wangeditor.com/v5/for-frame.html
在components文件夹下新建组件RichTextEditor,按照文档要求编写组件,由于上传本地视频较麻烦,这里将它屏蔽掉
<template><div><div style="border: 1px solid #ccc; margin-top: 10px"><Toolbar:editor="editorRef":defaultConfig="toolbarConfig":mode="mode"style="border-bottom: 1px solid #ccc"/><Editor:defaultConfig="editorConfig":mode="mode"v-model="valueHtml"style="height: 400px; overflow-y: hidden"@onCreated="handleCreated"@onChange="handleChange"/></div></div>
</template><script setup>
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import { ref,reactive,inject,onMounted,onBeforeUnmount, shallowRef } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'// 服务端地址
const serverUrl = inject("serverUrl")// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef();
// 模式
const mode = ref("default")
// 内容HTML
const valueHtml = ref("")
//菜单栏配置
const toolbarConfig = { excludeKeys:["uploadVideo"] };
// 编辑器配置
const editorConfig = { placeholder: '请输入内容...', MENU_CONF: {} };
// 上传图片
editorConfig.MENU_CONF = {}
editorConfig.MENU_CONF['uploadImage'] = {// 小于该值就插入 base64 格式(而不上传),默认为 0base64LimitSize: 10 * 1024, // 10kbserver: serverUrl+"/upload/rich_editor_upload",
}
// 插入图片
editorConfig.MENU_CONF['insertImage'] = {parseImageSrc:(src) => {console.log(serverUrl, src)if (src.indexOf("http") != 0){return `${serverUrl}${src}`}return src}
}
// 定义属性进行双向绑定
const props = defineProps({modelValue:{type:String,default:""}
})
// 定义抛出事件
const emit = defineEmits(["update:model-value"])
// 模拟 ajax 异步获取内容
onMounted(() => {setTimeout(() => {valueHtml.value = props.modelValueinitFinished = true }, 10)
})
let initFinished = false
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {const editor = editorRef.valueif (editor == null) returneditor.destroy()
})// 编辑器回调函数
const handleCreated = (editor) => {editorRef.value = editor // 记录 editor 实例,重要!
}
const handleChange = (editor) => {if (initFinished) {emit("update:model-value", valueHtml.value) // 输入时往外抛}
};</script><style lang="scss" scoped></style>
(七)文章发布修改页
文章发布与修改页类似,不同的是修改页要先获取原文章数据再将其渲染
Publish.vue
<template><div class="topbar"><n-button @click="goback" strong quaternary round style="position: absolute; left: 50px; top: 7px; font-size: 24px;" color="#7B3DE0"><n-icon><return-up-back /></n-icon></n-button><text style="position:absolute; left: 200px; line-height: 50px; color: #383838">标题</text><n-input v-model:value="addArticle.title" round placeholder="请输入标题" style="position:absolute; left: 265px; top: 8px; width: 1000px; background-color: #F3F0F9;" /><n-avatar round size="medium" :src=user.avatarUrl style="position: absolute; right: 190px; top: 8px; "/><div style="position: absolute; right: 50px; top: 8px"><n-button round color="#7B3DE0" @click="showModalModal"><template #icon><n-icon><send /></n-icon></template>发布</n-button></div> </div><div class="tabs"><n-card><rich-text-editor v-model:modelValue="addArticle.content"></rich-text-editor></n-card></div><n-modal v-model:show="showModal"><div style="width: 400px; height: 450px; background: white;"><n-card title="封面" :bordered="false" ><div v-if="!newHeadImage" style="width: 300px; height: 150px; margin: 0 auto;"><n-uploadmultipledirectory-dnd:max="1"@before-upload="beforeUpload":custom-request="customRequest"><n-upload-dragger><div style="margin-bottom: 12px"><n-icon size="48" :depth="3"><archive-icon /></n-icon></div><n-text style="font-size: 16px">点击或者拖动图片到此处</n-text></n-upload-dragger></n-upload></div><div v-else style="width: 230px; margin: 0 auto;"><n-image height="150" width="300" :src=serverUrl+addArticle.headImage /> <n-button @click="deleteImage" circle style="position: absolute; left: 298px; top: 50px;" color="#383838"><template #icon><n-icon><close /></n-icon></template></n-button> </div></n-card><n-card title="分类" :bordered="false"><div style="width:300px; margin: 0 auto;"><n-select v-model:value="addArticle.categoryId" :options="categoryOptions" placeholder="请选择分类"/></div></n-card><div style="position: absolute; right: 100px; bottom: 30px;"><n-button type="default" @click="closeSubmitModal">取消</n-button></div><div style="position: absolute; right: 30px; bottom: 30px;"><n-button type="primary" @click="submit">确认</n-button></div> </div></n-modal></template><script setup>
import {ref,reactive,inject, onMounted} from 'vue'
import RichTextEditor from '../components/RichTextEditor.vue'
import { ArchiveOutline as ArchiveIcon } from "@vicons/ionicons5"
import { Send } from "@vicons/ionicons5"
import { ReturnUpBack } from "@vicons/ionicons5"
import { Close } from "@vicons/ionicons5"import {useRouter, useRoute} from 'vue-router'
const router = useRouter()
const route = useRoute()const serverUrl = inject("serverUrl")
const axios = inject("axios")
const message = inject("message")const login = ref(false)
const user = reactive({avatarUrl: "",id: 0
})
const categoryOptions = ref([])
const addArticle = reactive({id: 0,categoryId: 0,title:"",content:"",headImage:"",
})
const showModal = ref(false)
const newHeadImage = ref(false)onMounted(() => {loadAvatar()loadCategories()
})const loadAvatar= async() => {let res = await axios.get("/user") console.log(res)if (res.data.code == 200) {user.avatarUrl = serverUrl + res.data.data.avataruser.id = res.data.data.idlogin.value = true}
} const loadCategories = async() =>{let res = await axios.get("/category")console.log(res)categoryOptions.value = res.data.data.categories.map((item)=>{return {label:item.name,value:item.id}})
}const showModalModal = () => {showModal.value = true
}const closeSubmitModal = () => {showModal.value = false
}const beforeUpload = async(data) => {if (data.file.file?.type !== "image/png") {message.error("只能上传png格式的图片")return false;}return true;
}const customRequest = async({file}) => {const formData = new FormData()formData.append('file', file.file)let res = await axios.post("/upload", formData)console.log(res)addArticle.headImage = res.data.data.filePathnewHeadImage.value = true
}const deleteImage = () => {addArticle.headImage = ""newHeadImage.value = false
}const submit = async() => {let res = await axios.post("/article", {category_id: addArticle.categoryId,title: addArticle.title,content: addArticle.content,head_image: addArticle.headImage})console.log(res) if (res.data.code == 200) {message.success(res.data.msg) goback() } else {message.error(res.data.msg)}
}const goback= () => {router.go(-1)
}</script><style lang="scss" scoped>
.topbar {position: sticky;top: 0;height: 50px;background: white;box-shadow: 0px 1px 5px #D3D4D8;
}
.tabs {position: absolute;top: 75px;left: 0;right: 0;margin: auto;width: 1000px;height: auto;background: white; box-shadow: 0px 1px 3px #D3D4D8; border-radius: 5px;
}
</style>
Updata.vue
<template><div class="topbar"><n-button @click="goback" strong quaternary round style="position: absolute; left: 50px; top: 7px; font-size: 24px;" color="#7B3DE0"><n-icon><return-up-back /></n-icon></n-button><text style="position:absolute; left: 200px; line-height: 50px; color: #383838">标题</text><n-input v-model:value="updateArticle.title" round placeholder="请输入标题" style="position:absolute; left: 265px; top: 8px; width: 1000px; background-color: #F3F0F9;" /><n-avatar round size="medium" :src=user.avatarUrl style="position: absolute; right: 190px; top: 8px; "/><div style="position: absolute; right: 50px; top: 8px"><n-button round color="#7B3DE0" @click="showModalModal"><template #icon><n-icon><send /></n-icon></template>发布</n-button></div> </div><div class="tabs"><n-card><rich-text-editor v-if="loadOk" v-model:modelValue="updateArticle.content"></rich-text-editor></n-card></div><n-modal v-model:show="showModal"><div style="width: 400px; height: 450px; background: white;"><n-card title="封面" :bordered="false" ><div v-if="!newHeadImage" style="width: 300px; height: 150px; margin: 0 auto;"><n-uploadmultipledirectory-dnd:max="1"@before-upload="beforeUpload":custom-request="customRequest"><n-upload-dragger><div style="margin-bottom: 12px"><n-icon size="48" :depth="3"><archive-icon /></n-icon></div><n-text style="font-size: 16px">点击或者拖动图片到此处</n-text></n-upload-dragger></n-upload></div><div v-else style="width: 230px; margin: 0 auto;"><n-image height="150" width="300" :src=serverUrl+updateArticle.headImage /> <n-button @click="deleteImage" circle style="position: absolute; left: 298px; top: 50px;" color="#383838"><template #icon><n-icon><close /></n-icon></template></n-button> </div></n-card><n-card title="分类" :bordered="false"><div style="width:300px; margin: 0 auto;"><n-select v-model:value="updateArticle.categoryId" :options="categoryOptions" placeholder="请选择分类"/></div></n-card><div style="position: absolute; right: 100px; bottom: 30px;"><n-button type="default" @click="closeSubmitModal">取消</n-button></div><div style="position: absolute; right: 30px; bottom: 30px;"><n-button type="primary" @click="submit">确认</n-button></div> </div></n-modal></template><script setup>
import { ArchiveOutline as ArchiveIcon } from "@vicons/ionicons5"
import { Send } from "@vicons/ionicons5"
import { ReturnUpBack } from "@vicons/ionicons5"
import { Close } from "@vicons/ionicons5"
import {ref,reactive,inject, onMounted} from 'vue'
import RichTextEditor from '../components/RichTextEditor.vue'import {useRouter, useRoute} from 'vue-router'
const router = useRouter()
const route = useRoute()const serverUrl = inject("serverUrl")
const axios = inject("axios")
const message = inject("message")const loadOk = ref(false)
const user = reactive({avatarUrl: "",id: 0
})
const categoryOptions = ref([])
const updateArticle = reactive({id: 0,categoryId: 0,title:"",content:"",headImage:"",
})
const showModal = ref(false)
const newHeadImage = ref(false)onMounted(() => {loadAvatar()loadCategories()loadArticle()
})const loadAvatar= async() => {let res = await axios.get("/user") console.log(res)if (res.data.code == 200) {user.avatarUrl = serverUrl + res.data.data.avataruser.id = res.data.data.id}
} const loadCategories = async() =>{let res = await axios.get("/category")console.log(res)categoryOptions.value = res.data.data.categories.map((item)=>{return {label:item.name,value:item.id}})
}const loadArticle = async() => {let res = await axios.get("/article/" + route.query.id)console.log(res)if (res.data.code == 200) {updateArticle.categoryId = res.data.data.article.categoryId,updateArticle.title = res.data.data.article.title,updateArticle.content = res.data.data.article.content,updateArticle.headImage = res.data.data.article.headImage,newHeadImage.value = updateArticle.headImage? true: false loadOk.value = true }
}const showModalModal = () => {showModal.value = true
}const closeSubmitModal = () => {showModal.value = false
}const beforeUpload = async(data) => {if (data.file.file?.type !== "image/png") {message.error("只能上传png格式的图片")return false;}return true;
}const customRequest = async({file}) => {const formData = new FormData()formData.append('file', file.file)let res = await axios.post("/upload", formData)console.log(res)updateArticle.headImage = res.data.data.filePathnewHeadImage.value = true
}const deleteImage = () => {updateArticle.headImage = ""newHeadImage.value = false
}const submit = async() => {let res = await axios.put("/article/" + route.query.id, {category_id: updateArticle.categoryId,title: updateArticle.title,content: updateArticle.content,head_image: updateArticle.headImage})console.log(res) if (res.data.code == 200) {message.success(res.data.msg) goback() } else {message.error(res.data.msg)}
}const goback= () => {router.go(-2)
}</script><style lang="scss" scoped>
.topbar {position: sticky;top: 0;height: 50px;background: white;box-shadow: 0px 1px 5px #D3D4D8;
}
.tabs {position: absolute;top: 75px;left: 0;right: 0;margin: auto;width: 1000px;height: auto;background: white; box-shadow: 0px 1px 3px #D3D4D8; border-radius: 5px;
}
</style>
(八)文章详情页
在文章详情页中展示文章标题、内容、分类、作者头像等内容,这里需要判断查看文章详情的是否是作者,如果是的话添加编辑和删除按键
<template><div style="position: fixed; top: 0; height: 50px; width: 100%; z-index: 999; "><TopBar></TopBar></div><div class="tabs"><n-card><n-h1>{{articleInfo.title}}</n-h1><div style="height: 75px; background-color: #FCFAF7;"><n-avatar @click="toOtherUser" round size="medium" :src=userUrl style="position: relative; left: 20px; top: 20px; cursor: pointer;"/><text style="position: relative; left: 36px; color: #808080;">发布时间:{{articleInfo.createdAt}} </text><div style="position: relative; left: 70px; color: #808080;">文章分类:<n-tag type="warning">{{categoryName}}</n-tag></div><n-button v-if="self" @click="toUpdate" ghost style="bottom: 45px; left: 805px;" color="#7B3DE0">修改</n-button><n-button v-if="self" @click="toDelete" ghost style="bottom: 45px; left: 815px;" color="#7B3DE0">删除</n-button></div><n-divider /><n-button v-if=!collected text @click="newCollect" style="position: absolute; right: 40px; top: 25px; cursor: pointer;" round ghost color="#FFA876"><template #icon><n-icon><star-outline /></n-icon></template>收藏</n-button><n-button v-else text @click="unCollect" style="position: absolute; right: 40px; top: 25px; cursor: pointer;" round ghost color="#FFA876"><template #icon><n-icon><star /></n-icon></template>已收藏</n-button><div class="article-content"><div v-html="articleInfo.content"></div></div></n-card></div>
</template><script setup>
import {ref,reactive,inject, onMounted} from 'vue'
import TopBar from '../components/TopBar.vue'
import { Star } from "@vicons/ionicons5"
import { StarOutline } from "@vicons/ionicons5"import {useRouter, useRoute} from 'vue-router'
const router = useRouter()
const route = useRoute()const serverUrl = inject("serverUrl")
const axios = inject("axios")
const message = inject("message")
const dialog = inject("dialog")const articleInfo = ref({})
const categoryName = ref("")
const user = ref({})
const userUrl = ref("")
const collected = ref(false)
const index = ref(0)
const self = ref(false)onMounted(() => {loadArticle()
})const loadArticle = async() => {let res1 = await axios.get("article/" + route.query.id)console.log(res1)if (res1.data.code == 200) {articleInfo.value = res1.data.data.article let res2 = await axios.get("category/" + res1.data.data.article.category_id) console.log(res2)if (res2.data.code == 200) {categoryName.value = res2.data.data.categoryName}let res3 = await axios.get("user/briefInfo/" + res1.data.data.article.user_id)console.log(res3)if (res3.data.code == 200) {user.value = res3.data.datauserUrl.value = serverUrl + user.value.avatarif (user.value.id == user.value.loginId) {self.value = true}} let res4 = await axios.get("collects/" + route.query.id) console.log(res4)if (res4.data.code == 200) {collected.value = res4.data.data.collectedindex.value = res4.data.data.index} }
}const newCollect = async() => {let res = await axios.put("collects/new/" + route.query.id)console.log(res) if (res.data.code == 200) {message.warning("已收藏", {showIcon: false}) loadArticle() }
}const unCollect = async() => {let res = await axios.delete("collects/" + index.value)console.log(res) if (res.data.code == 200) {message.warning("取消收藏", {showIcon: false}) loadArticle() }
}const toOtherUser = () => {if (user.value.id == user.value.loginId) {router.push({path: "/myself",query: {id: user.value.id}}) } else {router.push({path: "/others",query: {id: user.value.id}})}
}const toUpdate = () => {router.push({path: "/update",query: {id: articleInfo.value.id}})
}const toDelete = async (blog) => {dialog.warning({title: '警告',content: '是否要删除',positiveText: '确定',negativeText: '取消',onPositiveClick: async () => {let res = await axios.delete("article/" + articleInfo.value.id)if(res.data.code == 200){message.info(res.data.msg)goback()}else{message.error(res.data.msg)} },onNegativeClick: () => {}})
}const goback= () => {router.go(-1)
}</script><style lang="scss" scoped>
.tabs {position: absolute;top: 75px;left: 0;right: 0;margin: auto;width: 1000px;height: auto;background: white; box-shadow: 0px 1px 3px #D3D4D8; border-radius: 5px;
}
.article-content img{max-width: 100% !important;
}
</style>
(九)总结
恭喜你已完成整个项目的搭建,完结撒花~~
全栈开发实战(二)——简易博客社区前端搭建教程(附源码)相关推荐
- 全栈开发实战(一)——简易博客社区后端搭建教程(附源码)
全栈开发实战(一)--简易博客社区后端搭建 项目展示视频 项目Github地址 (一)项目准备 在项目开始前,首先确保你已安装好Go语言并配置好Go语言编辑器,同时安装好MySQL或其他数据库,其次, ...
- CSDN博客文章阅读模式插件(附源码)
插件地址:https://greasyfork.org/zh-CN/scripts/380667-csdn%E5%8D%9A%E5%AE%A2%E9%98%85%E8%AF%BB%E6%A8%A1%E ...
- Visual C++实现黑白棋游戏项目实战二:界面的设计与实现(附源码和资源 超详细)
需要源码和资源请点赞关注收藏后评论区留言私信~~~ 黑白棋游戏的Visual C++工程采用MFC对话框模式进行开发,下面对它进行详细介绍 一.游戏菜单的实现 首先要在工程资源中添加一个菜单资源类,菜 ...
- Android App开发实战项目之大头贴App功能实现(附源码和演示 简单易上手)
需要图片集和源码请点赞关注收藏后评论区留言~~~ 一.需求描述 大头贴App有两个特征,第一个是头要大,拿来一张照片后把人像区域裁剪出来,这样新图片里的人头才会比较大,第二个是在周围贴上装饰物,而且装 ...
- Spring Boot 专栏全栈开发实战
2020 年 11 月 12 日,Spring 官方发布了 Spring Boot 2.4.0 GA 的公告,链接为 Spring Boot 2.4.0 available now.为了让大家能够学习 ...
- 【哈士奇赠书活动 - 18期】-〖Flask Web全栈开发实战〗
文章目录 ⭐️ 赠书活动 - <Flask Web全栈开发实战> ⭐️ 编辑推荐 ⭐️ 内容提要 ⭐️ 赠书活动 → 获奖名单 ⭐️ 赠书活动 - <Flask Web全栈开发实战& ...
- 【Python开发】Flask开发实战:个人博客(三)
Flask开发实战:个人博客(三) 在[Python开发]Flask开发实战:个人博客(一) 中,我们已经完成了 数据库设计.数据准备.模板架构.表单设计.视图函数设计.电子邮件支持 等总体设计的内容 ...
- Spring Boot+Vue全栈开发实战——花了一个礼拜读懂了这本书
很幸运能够阅读王松老师的<Spring Boot+Vue全栈开发实战>这本书!之前也看过Spring Boot与Vue的相关知识,自己也会使用了Spring Boot+Vue进行开发项目. ...
- ChatGPT全栈开发实战:从需求分析到数据可视化,一站式指南助你快速构建全面应用
<ChatGPT全栈开发实战:从需求分析到数据可视化,一站式指南助你快速构建全面应用>是一本旨在引领我们进入全栈开发世界的综合指南.通过结合强大的ChatGPT技术和全栈开发的实践,我们将 ...
- 读书笔记《Spring Boot+Vue全栈开发实战》(下)
本书将带你全面了解Spring Boot基础与实践,带领读者一步步进入 Spring Boot 的世界. 前言 第九章 Spring Boot缓存 第十章 Spring Boot安全管理 第十一章 S ...
最新文章
- 求二叉树第K层的节点个数+求二叉树叶子节点的个数
- [JS] - onmusewheel事件(兼容IE,FF,opera,safari,chrome)
- 转:Ubuntu下ibus-sunpinyin的安装及翻页快捷键设置!
- LAMP 系统性能调优,第 3 部分: MySQL 服务器调优(转)
- php class setter,设置器 - Setters《 PHP 面向对象 》
- RSYNC及其算法简单介绍
- leetcode python3 简单题101. Symmetric Tree
- 卸载软件 Geek Uninstaller
- 用keil怎么擦除_环氧树脂结构胶怎么清洗 结构胶弄到衣服上怎么洗掉
- 计算机不断自动重启,电脑不断自动重启怎么办?
- [PTA] 7-11 计算平均分
- 论坛社区小程序(前段+后端)安装教程
- 我在哪?要到哪里去?怎么去?
- Laplacian of Gaussian公式的英文推导过程
- 测试体重的手机软件,手机能测重量的软件
- Django连接mysql数据库操作
- 【华为机试真题 Python】素数之积
- SpringBoot集成EMail
- 读取excel中数据时,数字格式发生改变
- 51单片机能否实现硬件仿真