全栈开发实战(二)——简易博客社区前端搭建教程(附源码)
在开始我们的项目前,请确保你已安装好node.js及vue3.js,并配置好相应的编辑器,本项目所使用的编辑器为Visual Studio Code在终端输入以下语句使用vite创建项目blog_client:cd进入输入以下语句安装必要的依赖:输入以下语句运行项目:本项目所使用的模块如下安装上述模块将src/style.css修改为
全栈开发实战(二)——简易博客社区前端搭建
(一)项目准备
在开始我们的项目前,请确保你已安装好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
npm install vue-router@4
npm i -D naive-ui
npm i -D vfonts
npm i -D @vicons/ionicons5
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" // 引入UserStore
axios.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()); // 引入pinia
app.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"; // 引入pinia
export 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.token
if (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.avatar
user.id = res.data.data.id
login.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" // 引入UserStore
axios.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()); // 引入pinia
const 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-upload
multiple
directory-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.self
user.avatarUrl = serverUrl + res.data.data.avatar
user.name = res.data.data.name
user.number = res.data.data.articles.length
user.fans = res.data.data.fans
user.id = res.data.data.id
articles.value = res.data.data.articles
collects.value = res.data.data.collects
following.value = res.data.data.following
newName.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.filePath
newAvatar.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.self
user.avatarUrl = serverUrl + res1.data.data.avatar
user.name = res1.data.data.name
user.number = res1.data.data.articles.length
user.fans = res1.data.data.fans
user.id = res1.data.data.id
user.loginId = res1.data.data.loginId
articles.value = res1.data.data.articles
collects.value = res1.data.data.collects
following.value = res1.data.data.following
let res2 = await axios.get("following/" + route.query.id)
console.log(res2)
if (res2.data.code == 200) {
followed.value = res2.data.data.followed
index.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 = categoryId
pageInfo.pageNum = 1
loadArticles()
}
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 格式(而不上传),默认为 0
base64LimitSize: 10 * 1024, // 10kb
server: 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.modelValue
initFinished = true
}, 10)
})
let initFinished = false
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.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-upload
multiple
directory-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.avatar
user.id = res.data.data.id
login.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.filePath
newHeadImage.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-upload
multiple
directory-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.avatar
user.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.filePath
newHeadImage.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.data
userUrl.value = serverUrl + user.value.avatar
if (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.collected
index.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>
(九)总结
恭喜你已完成整个项目的搭建,完结撒花~~
更多推荐
所有评论(0)