黑马大事故前端
前端最终源代码见本文末
视频链接:黑马程序员SpringBoot3+Vue3全套视频教程
一、情况预备
1.1 创建vue工程(big-event)
- npm init vue@latest
- cd big-event
- npm install
复制代码 1.2 安装插件
- 安装element-plus
- 1.1 执行命令: npm install element-plus --save
- 1.2 在main.js中做如下配置
- import ElementPlus from 'element-plus'
- import 'element-plus/dist/index.css'
- app.use(ElementPlus)
复制代码 - 安装axios
- 安装sass依靠
1.3 目录调解
- 删除components目录下的内容
- 删除App.vue中的内容,只保留script和template标签
- 新建如下目录:
api:存放接口调用的js文件
utils:存放工具js文件
拷贝request.js到util目录
views:存放页面的.vue文件
- 删除assets目录中的内容, 将资料中的静态资源文件全部拷贝到该目录下
二、注册
2.1 页面搭建
2.2 页面数据绑定与事故绑定
2.2.1 数据绑定
- //用于注册的数据模型
- const registerData = ref({
- username: '',
- password: '',
- rePassword: ''
- })
复制代码 2.2.2 表单校验
- //自定义确认密码的校验函数
- const rePasswordValid = (rule, value, callback) => {
- if (value == null || value === '') {
- return callback(new Error('请再次确认密码'))
- }
- if (registerData.password !== value) {
- return callback(new Error('两次输入密码不一致'))
- }
- }
- //用于注册的表单校验模型
- const registerDataRules = ref({
- username: [
- { required: true, message: '请输入用户名', trigger: 'blur' },
- { min: 5, max: 16, message: '用户名的长度必须为5~16位', trigger: 'blur' }
- ],
- password: [
- { required: true, message: '请输入密码', trigger: 'blur' },
- { min: 5, max: 16, message: '密码长度必须为5~16位', trigger: 'blur' }
- ],
- rePassword: [
- { validator: rePasswordValid, trigger: 'blur' }
- ]
- })
复制代码 2.2.3 事故绑定
- //用于注册的事件函数
- const register = () => {
- console.log('注册...');
- }
复制代码 2.3 接口调用
2.3.1 在src/api/user.js中提供访问注册接口的函数
- //注册
- export const registerService = (registerData) => {
- var params = new URLSearchParams()
- for (let key in registerData) {
- params.append(key, registerData[key])
- }
- return request.post('/user/register', params)
- }
复制代码 2.3.2 在Login.vue中完成注册接口调用
- import { registerService} from '@/api/user.js'
- //用于注册的事件函数
- const register = async () => {
- //console.log('注册...');
- let result = await registerService(registerData.value);
- if (result.code == 0) {
- alert('注册成功!')
- } else {
- alert('注册失败!')
- }
- }
复制代码 2.4 处理跨域问题
由于发起ajax请求的域为http://localhost:5173, 而后台服务器的域为 http://localhost:8080, 所以欣赏器会限定该请求的发送, 这种问题称为跨域问题, 跨域问题可以在服务器端办理,也可以在欣赏器端办理, 咱们这一块通过设置署理的方式办理
request.js中设置统一前缀 /api
- //定制请求的实例//导入axios npm install axios
- import axios from 'axios';//定义一个变量,记录公共的前缀 , baseURLconst baseURL = '/api';const instance = axios.create({baseURL})//添加相应拦截器instance.interceptors.response.use( result=>{ return result.data; }, err=>{ alert('服务异常'); return Promise.reject(err);//异步的状态转化成失败的状态 })export default instance;
复制代码 vie.config.js中设置署理
- import { fileURLToPath, URL } from 'node:url'
- import { defineConfig } from 'vite'
- import vue from '@vitejs/plugin-vue'
- // https://vitejs.dev/config/
- export default defineConfig({
- plugins: [
- vue(),
- ],
- resolve: {
- alias: {
- '@': fileURLToPath(new URL('./src', import.meta.url))
- }
- },
- //配置代理
- server: {
- proxy: {
- '/api': {
- target: 'http://localhost:8080', // 后端服务器地址
- changeOrigin: true, // 是否改变请求域名
- rewrite: (path) => path.replace(/^\/api/, '')//将原有请求路径中的api替换为''
- }
- }
- }
- })
复制代码 三、登录
3.1页面数据绑定与事故绑定
3.1.1 绑定数据
- //复用注册表单的数据模型
- const registerData = ref({
- username: '',
- password: '',
- rePassword: ''
- })
复制代码 3.1.2 每次点击注册或者登录,共用数据模型中的数据
- //清空数据模型的数据
- const clearRegisterData = () => {
- registerData.value = {
- username: '',
- password: '',
- rePassword: ''
- }
- }
复制代码 3.1.3 事故绑定
3.2 接口调用
3.2.1 在src/api/user.js中提供访问注册接口的函数
- //登录
- export const loginService = (loginData)=>{
- var params = new URLSearchParams()
- for(let key in loginData){
- params.append(key,loginData[key])
- }
- return request.post('/user/login',params)
- }
复制代码 3.2.2 在Login.vue中完成登录接口调用
- import { registerService, loginService } from '@/api/user.js'
- //用于登录的事件函数
- const login = async () => {
- let result = await loginService(registerData.value)
- if(result.code==0){
- alert('登录成功!')
- }else{
- alert('登录失败!')
- }
- }
复制代码 四、优化axios相应截器
在接口调用的API中,我们都需要对业务相应的状态举行判断,从而给用户对应的提示,这个工作不难,但是每个接口的调用,都如许写代码,显然是比力繁琐的,我们可以在axios的相应拦截器中,如果服务器相应乐成了,统一判断后台返回的业务状态码code,如果乐成了,正常返回数据,如果失败了,则给出用户对应的提示即可
请求工具request.js
- //添加响应拦截器
- instance.interceptors.response.use(
- result => {
- //如果业务状态码为0,代表本次操作成功
- if (result.data.code == 0) {
- return result.data;
- }
- //代码走到这里,代表业务状态码不是0,本次操作失败
- alert(result.data.message || '服务异常');
- return Promise.reject(result.data);//异步的状态转化成失败的状态
- },
- err => {
- alert('服务异常');
- return Promise.reject(err);//异步的状态转化成失败的状态
- }
- )
复制代码 接口调用user.js
- //用于注册的事件函数
- const register = async () => {
- //console.log('注册...');
- await registerService(registerData.value);
- alert('注册成功!')
- }
- //用于登录的事件函数
- const login = async () => {
- await loginService(registerData.value)
- alert('登录成功!')
-
- }
复制代码 Element-Plus提示框的使用
- import { ElMessage } from 'element-plus'
- ElMessage.error('服务异常');
- ElMessage.success('登录成功!')
复制代码 五、主页面布局
六、路由
在App.vue中,不能同时展示Login.vue和Layout.vue,实际的需求是用户第一次访问程序,先展示登录页面,当用户登录乐成后,再展示主页面,如果要达成这个需求,需要用到vue提供的路由相干的知识
路由,从出发点到尽头时,决定从出发点到尽头的路径的进程,在前端工程中,路由指的是根据不同的访问路径,展示不同组件的内容。Vue Router是Vue.js的官方路由,它与Vue.js深度集成,让Vue.js构建单页面应用变得更加十拿九稳
6.1安装路由
6.2创建路由器,并导出
在src/router目录下,定义一个js文件,起名为index.js。如许名字的js文件在导入时,可以不写文件名,只要定位到文件所在的文件夹即可,使用起来很方便
- //导入vue-router
- import { createRouter, createWebHistory } from 'vue-router'
- //导入组件
- import LoginVue from '@/views/Login.vue'
- import LayoutVue from '@/views/Layout.vue'
- //定义路由关系
- const routes = [
- { path: '/login', component: LoginVue },
- { path: '/', component: LayoutVue }
- ]
- //创建路由器
- const router = createRouter({
- history: createWebHistory(),
- routes: routes
- });
- export default router
复制代码 6.3在vue应用实例中使用router
在main.js中导入创建应用气力的js文件,并调用实例的use方法使用路由器
- import router from '@/router'
- app.use(router)
复制代码 6.4定义展示路由组件的地方
在App.vue文件的template标签中,定义router-view标签
- <template>
- <router-view></router-view>
- </template>
复制代码 将来不管根据路由匹配到的组件内容,会在router-view标签内举行展示
6.5 测试
在欣赏器地址栏分别访问:http://localhost:5173/ 和 http://localhost:5173/login
6.6 路由API
在登录乐成后,需要通过代码的方式将页面切换到首页,此时就需要调用路由器相干的API
获取路由器
- import { useRouter } from 'vue-router'
- const router = useRouter();
复制代码 调用API
七、子路由
在咱们的主页面中,当用户点击左侧的菜单时,右侧主地区的内容需要发生变化,将来每切换一个菜单,右侧需要加载对应组件的内容举行展示,像如许的场景咱们也需要使用路由来完成
由于这些组件都需要在Layout.vue中展示, 而Layout.vue本身已经参与了路由,因此我们需要在Layout.vue中通过子路由的方式来完成组件的切换
7.1提供菜单对应的组件
可以复制资料中的文件,也可以自己创建:
- ArticleCategory.vue
- ArticleManage.vue
- UserInfo.vue
- UserAvatar.vue
- UserResetPassword.vue
7.2设置子路由
在src/router/index.js中设置子路由
- //定义路由关系
- const routes = [
- { path: '/login', component: LoginVue },
- {
- path: '/',
- component: LayoutVue,
- //重定向
- redirect: '/article/manage',
- //子路由
- children: [
- { path: '/article/category', component: ArticleCategoryVue },
- { path: '/article/manage', component: ArticleManageVue },
- { path: '/user/info', component: UserInfoVue },
- { path: '/user/avatar', component: UserAvatarVUe },
- { path: '/user/password', component: UserResetPasswordVue },
- ]
- }
- ]
复制代码 7.3 在Layout.vue组件的右侧中间地区,添加router-view标签
- <!-- 中间区域 -->
- <el-main>
- <div style="width: 1290px; height: 570px;border: 1px solid red;">
- <router-view></router-view>
- </div>
- </el-main>
复制代码 7.4 菜单项设置点击后跳转的路由路径
el-menu-item 标签的index属性可以设置点击后的路由路径
- <el-menu-item index="/article/category">
- <el-icon>
- <Management />
- </el-icon>
- <span>文章分类</span>
- </el-menu-item>
复制代码 八、文章分类列表
8.1 文章分类组件
- <script setup>
- import {
- Edit,
- Delete
- } from '@element-plus/icons-vue'
- import { ref } from 'vue'
- const categorys = ref([
- {
- "id": 3,
- "categoryName": "美食",
- "categoryAlias": "my",
- "createTime": "2023-09-02 12:06:59",
- "updateTime": "2023-09-02 12:06:59"
- },
- {
- "id": 4,
- "categoryName": "娱乐",
- "categoryAlias": "yl",
- "createTime": "2023-09-02 12:08:16",
- "updateTime": "2023-09-02 12:08:16"
- },
- {
- "id": 5,
- "categoryName": "军事",
- "categoryAlias": "js",
- "createTime": "2023-09-02 12:08:33",
- "updateTime": "2023-09-02 12:08:33"
- }
- ])
- </script>
- <template>
- <el-card class="page-container">
- <template #header>
- <div class="header">
- <span>文章分类</span>
- <div class="extra">
- <el-button type="primary">添加分类</el-button>
- </div>
- </div>
- </template>
- <el-table :data="categorys" style="width: 100%">
- <el-table-column label="序号" width="100" type="index"> </el-table-column>
- <el-table-column label="分类名称" prop="categoryName"></el-table-column>
- <el-table-column label="分类别名" prop="categoryAlias"></el-table-column>
- <el-table-column label="操作" width="100">
- <template #default="{ row }">
- <el-button :icon="Edit" circle plain type="primary" ></el-button>
- <el-button :icon="Delete" circle plain type="danger"></el-button>
- </template>
- </el-table-column>
- <template #empty>
- <el-empty description="没有数据" />
- </template>
- </el-table>
- </el-card>
- </template>
- <style lang="scss" scoped>
- .page-container {
- min-height: 100%;
- box-sizing: border-box;
- .header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- }
- </style>
复制代码 8.2 列表接口调用
src/api/article.js
- //导入请求工具类
- import request from '@/utils/request.js'
- //文章分类列表查询
- export const articleCategoryListService = ()=>{
- return request.get('/category')
- }
复制代码 ArticleCategory.vue
- //获取所有文章分类数据
- import { articleCategoryListService } from '@/api/article.js'
- const getAllCategory = async () => {
- let result = await articleCategoryListService();
- categorys.value = result.data;
- }
- getAllCategory();
复制代码 但是上述的代码并不能真正的获取到所有文章分类数据,服务器相应状态码为401,由于目前请求头中并没有携带token
九、Pinia状态管理库
Pinia是Vue的专属状态管理库,它答应你跨组件或页面共享状态
9.1安装
9.2使用Pinia
在main.js中,引入pinia,创建pinia实例,并调用vue应用实例的use方法使用pinia
- import { createPinia } from 'pinia'
- const pinia = createPinia()
- app.use(pinia)
复制代码 9.3 定义Store
在src/stores目录下定义token.js
- import { defineStore } from "pinia";
- import {ref} from 'vue';
- /*
- defineStore参数描述:
- 第一个参数:给状态起名,具有唯一性
- 第二个参数:函数,可以把定义该状态中拥有的内容
- defineStore返回值描述:
- 返回的是一个函数,将来可以调用该函数,得到第二个参数中返回的内容
- */
- export const useTokenStore = defineStore('token',()=>{
- //1.定义描述token
- const token = ref('')
- //2.定义修改token的方法
- const setToken = (newToken)=>{
- token.value = newToken
- }
- //3.定义移除token的方法
- const removeToken = ()=>{
- token.value=''
- }
- return {
- token,setToken,removeToken
- }
- })
复制代码 9.4 使用Store
在需要使用状态的地方,导入@/stores/*.js , 使用即可
在Login.vue中导入@/stores/token.js, 并且当用户登录乐成后,将token生存pinia中
- //导入token状态import { useTokenStore } from '@/stores/token.js'//调用useTokenStore得到状态const tokenStore = useTokenStore();//用于登录的事故函数const login = async () => { let result = await loginService(registerData.value) //生存token tokenStore.setToken(result.data) ElMessage.success('登录乐成!') router.push('/')
- }
复制代码 在article.js中导入@/stores/token.js, 从pinia中获取到存储的token,在发起查询文章分类列表的时候把token通过请求头的形式携带给服务器
- //导入@/stores/token.js
- import { useTokenStore } from '../stores/token'
- //文章分类列表查询
- export const articleCategoryListService = () => {
- //获取token状态
- const tokenStore = useTokenStore()
- //通过请求头Authorization携带token
- return request.get('/category', { headers: { 'Authorization': tokenStore.token } })
- }
复制代码 十、axios请求拦截器
当进入主页后,将来要与后台交互,都需要携带token,如果每次请求都写如许的代码,将会比力繁琐,此时可以将携带token的代码通过请求拦截器统一处理
在 src/util/request.js中
- //导入token状态
- import { useTokenStore } from '@/stores/token.js';
- //添加请求拦截器
- instance.interceptors.request.use(
- (config)=>{
- //在发送请求之前做什么
- let tokenStore = useTokenStore()
- //如果token中有值,在携带
- if(tokenStore.token){
- config.headers.Authorization=tokenStore.token
- }
- return config
- },
- (err)=>{
- //如果请求错误做什么
- Promise.reject(err)
- }
- )
复制代码 十一、Pinia长期化插件
默认情况下,由于pinia是内存存储,当你刷新页面的时候pinia中的数据会丢失,可以借助于persist插件办理这个问题,persist插件支持将pinia中的数据长期化到sessionStorage和localStorage中
11.1 安装persist插件
- npm install pinia
- -persistedstate-plugin
复制代码 11.2 pinia中使用persist插件
在main.js中
- import { createPinia } from 'pinia'
- //导入持久化插件
- import {createPersistedState} from'pinia-persistedstate-plugin'
- const pinia = createPinia()
- const persist = createPersistedState()
- //pinia使用持久化插件
- pinia.use(persist)
- app.use(pinia)
复制代码 11.3 在创建定义状态是设置长期化
在src/stores/token.js中
- export const useTokenStore = defineStore('token',()=>{
- //1.定义描述token
- const token = ref('')
- //2.定义修改token的方法
- const setToken = (newToken)=>{
- token.value = newToken
- }
- //3.定义移除token的方法
- const removeToken = ()=>{
- token.value=''
- }
- return {
- token,setToken,removeToken
- }
- }
- ,
- //参数持久化
- {
- persist:true
- }
- )
复制代码 十二、未登录统一处理
在后续访问接口时,如果没有登录,则前端不携带token,后台服务器会返回相应状态码401,代表未登录,此时可以在axios的相应拦截器中,统一对未登录的情况做处理
request.js
- import router from '@/router'
- //添加响应拦截器
- instance.interceptors.response.use(
- result => {
- //如果业务状态码为0,代表本次操作成功
- if (result.data.code == 0) {
- return result.data;
- }
- //代码走到这里,代表业务状态码不是0,本次操作失败
- ElMessage.error(result.data.message || '服务异常');
- return Promise.reject(result.data);//异步的状态转化成失败的状态
- },
- err => {
- //如果响应状态码时401,代表未登录,给出对应的提示,并跳转到登录页
- if(err.response.status===401){
- ElMessage.error('请先登录!')
- router.push('/login')
- }else{
- ElMessage.error('服务异常');
- }
- return Promise.reject(err);//异步的状态转化成失败的状态
- }
- )
复制代码 十三、添加文章分类
13.1 添加分类弹窗页面
- <!-- 添加分类弹窗 -->
- <el-dialog v-model="dialogVisible" title="添加弹层" width="30%">
- <el-form :model="categoryModel" :rules="rules" label-width="100px" style="padding-right: 30px">
- <el-form-item label="分类名称" prop="categoryName">
- <el-input v-model="categoryModel.categoryName" minlength="1" maxlength="10"></el-input>
- </el-form-item>
- <el-form-item label="分类别名" prop="categoryAlias">
- <el-input v-model="categoryModel.categoryAlias" minlength="1" maxlength="15"></el-input>
- </el-form-item>
- </el-form>
- <template #footer>
- <span class="dialog-footer">
- <el-button @click="dialogVisible = false">取消</el-button>
- <el-button type="primary"> 确认 </el-button>
- </span>
- </template>
- </el-dialog>
复制代码 13.2 数据模型和校验规则
- //控制添加分类弹窗
- const dialogVisible = ref(false)
- //添加分类数据模型
- const categoryModel = ref({
- categoryName: '',
- categoryAlias: ''
- })
- //添加分类表单校验
- const rules = {
- categoryName: [
- { required: true, message: '请输入分类名称', trigger: 'blur' },
- ],
- categoryAlias: [
- { required: true, message: '请输入分类别名', trigger: 'blur' },
- ]
- }
复制代码 13.3 添加分类按钮单击事故
- <el-button type="primary" @click="dialogVisible = true">添加分类</el-button>
复制代码 13.4 接口调用
在article.js中提供添加分类的函数
- //添加文章分类
- export const articleCategoryAddService = (categoryModel) => {
- return request.post('/category', categoryModel)
- }
复制代码 在页面中调用接口
- //访问后台,添加文章分类
- const addCategory = async ()=>{
- let result = await articleCategoryAddService(categoryModel.value);
- ElMessage.success(result.message? result.message:'添加成功')
- //隐藏弹窗
- dialogVisible.value = false
- //再次访问后台接口,查询所有分类
- getAllCategory()
- }
复制代码- <el-button type="primary" @click="addCategory"> 确认 </el-button>
复制代码 十四、修改文章分类
14.1 修改分类弹窗页面
修改分类弹窗和新增文章分类弹窗长的一样,所以可以服用添加分类的弹窗
弹窗标题显示
定义标题
- //弹窗标题
- const title=ref('')
复制代码 在弹窗上绑定标题
- <el-dialog v-model="dialogVisible" :title="title" width="30%">
复制代码 为添加分类按钮绑定事故
- <el-button type="primary" @click="title='添加分类';dialogVisible = true">添加分类</el-button>
复制代码 为修改分类按钮绑定事故
- <el-button :icon="Edit" circle plain type="primary" @click="title='修改分类';dialogVisible=true"></el-button>
复制代码 14.2 数据回显
当点击修改分类按钮时,需要把当前这一条数据的详细信息显示到修改分类的弹窗上,这个叫回显
通过插槽的方式得到被点击按钮所在行的数据
- <template #default="{ row }">
- <el-button :icon="Edit" circle plain type="primary" @click="updateCategoryEcho(row)"></el-button>
- <el-button :icon="Delete" circle plain type="danger"></el-button>
- </template>
复制代码 回显函数
- //修改分类回显
- const updateCategoryEcho = (row) => {
- title.value = '修改分类'
- dialogVisible.value = true
- //将row中的数据赋值给categoryModel
- categoryModel.value.categoryName=row.categoryName
- categoryModel.value.categoryAlias=row.categoryAlias
- //修改的时候必须传递分类的id,所以扩展一个id属性
- categoryModel.value.id=row.id
- }
复制代码 14.3 接口调用
article.js中提供修改分类的函数
- //修改分类
- export const articleCategoryUpdateService = (categoryModel)=>{
- return request.put('/category',categoryModel)
- }
复制代码 修改确定按钮的绑定事故
- <span class="dialog-footer">
- <el-button @click="dialogVisible = false">取消</el-button>
- <el-button type="primary" @click="title==='添加分类'? addCategory():updateCategory()"> 确认 </el-button>
- </span>
复制代码 调用接口完成修改的函数
- //修改分类
- const updateCategory=async ()=>{
- let result = await articleCategoryUpdateService(categoryModel.value)
- ElMessage.success(result.message? result.message:'修改成功')
- //隐藏弹窗
- dialogVisible.value=false
- //再次访问后台接口,查询所有分类
- getAllCategory()
- }
复制代码 由于现在修改和新增共用了一个数据模型,所以在点击添加分类后,偶然候会显示数据,此时可以将categoryModel中的数据清空
- //清空模型数据
- const clearCategoryModel = ()=>{
- categoryModel.value.categoryName='',
- categoryModel.value.categoryAlias=''
- }
复制代码 修改添加按钮的点击事故
- <el-button type="primary" @click="title = '添加分类'; dialogVisible = true;clearCategoryModel()">添加分类</el-button>
复制代码 十五、删除分类(后台需要补齐)
15.1 确认框
- //删除分类 给删除按钮绑定事件
- const deleteCategory = (row) => {
- ElMessageBox.confirm(
- '你确认删除该分类信息吗?',
- '温馨提示',
- {
- confirmButtonText: '确认',
- cancelButtonText: '取消',
- type: 'warning',
- }
- )
- .then(() => {
- //用户点击了确认
- ElMessage({
- type: 'success',
- message: '删除成功',
- })
- })
- .catch(() => {
- //用户点击了取消
- ElMessage({
- type: 'info',
- message: '取消删除',
- })
- })
- }
复制代码 15.2 接口调用
article.js中提供删除分类的函数
- //删除分类
- export const articleCategoryDeleteService = (id) => {
- return request.delete('/category?id='+id)
- }
复制代码 当用户点击确认后,调用接口删除分类
- //删除分类
- const deleteCategory = (row) => {
- ElMessageBox.confirm(
- '你确认删除该分类信息吗?',
- '温馨提示',
- {
- confirmButtonText: '确认',
- cancelButtonText: '取消',
- type: 'warning',
- }
- )
- .then(async () => {
- //用户点击了确认
- let result = await articleCategoryDeleteService(row.id)
- ElMessage.success(result.message?result.message:'删除成功')
- //再次调用getAllCategory,获取所有文章分类
- getAllCategory()
- })
- .catch(() => {
- //用户点击了取消
- ElMessage({
- type: 'info',
- message: '取消删除',
- })
- })
- }
复制代码 十六、文章列表
16.1 文章列表页面组件
- <script setup>
- import {
- Edit,
- Delete
- } from '@element-plus/icons-vue'
- import { ref } from 'vue'
- //文章分类数据模型
- const categorys = ref([
- {
- "id": 3,
- "categoryName": "美食",
- "categoryAlias": "my",
- "createTime": "2023-09-02 12:06:59",
- "updateTime": "2023-09-02 12:06:59"
- },
- {
- "id": 4,
- "categoryName": "娱乐",
- "categoryAlias": "yl",
- "createTime": "2023-09-02 12:08:16",
- "updateTime": "2023-09-02 12:08:16"
- },
- {
- "id": 5,
- "categoryName": "军事",
- "categoryAlias": "js",
- "createTime": "2023-09-02 12:08:33",
- "updateTime": "2023-09-02 12:08:33"
- }
- ])
- //用户搜索时选中的分类id
- const categoryId=ref('')
- //用户搜索时选中的发布状态
- const state=ref('')
- //文章列表数据模型
- const articles = ref([
- {
- "id": 5,
- "title": "陕西旅游攻略",
- "content": "兵马俑,华清池,法门寺,华山...爱去哪去哪...",
- "coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
- "state": "草稿",
- "categoryId": 2,
- "createTime": "2023-09-03 11:55:30",
- "updateTime": "2023-09-03 11:55:30"
- },
- {
- "id": 5,
- "title": "陕西旅游攻略",
- "content": "兵马俑,华清池,法门寺,华山...爱去哪去哪...",
- "coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
- "state": "草稿",
- "categoryId": 2,
- "createTime": "2023-09-03 11:55:30",
- "updateTime": "2023-09-03 11:55:30"
- },
- {
- "id": 5,
- "title": "陕西旅游攻略",
- "content": "兵马俑,华清池,法门寺,华山...爱去哪去哪...",
- "coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
- "state": "草稿",
- "categoryId": 2,
- "createTime": "2023-09-03 11:55:30",
- "updateTime": "2023-09-03 11:55:30"
- },
- ])
- //分页条数据模型
- const pageNum = ref(1)//当前页
- const total = ref(20)//总条数
- const pageSize = ref(3)//每页条数
- //当每页条数发生了变化,调用此函数
- const onSizeChange = (size) => {
- pageSize.value = size
- }
- //当前页码发生变化,调用此函数
- const onCurrentChange = (num) => {
- pageNum.value = num
- }
- </script>
- <template>
- <el-card class="page-container">
- <template #header>
- <div class="header">
- <span>文章管理</span>
- <div class="extra">
- <el-button type="primary">添加文章</el-button>
- </div>
- </div>
- </template>
- <!-- 搜索表单 -->
- <el-form inline>
- <el-form-item label="文章分类:">
- <el-select placeholder="请选择" v-model="categoryId">
- <el-option
- v-for="c in categorys"
- :key="c.id"
- :label="c.categoryName"
- :value="c.id">
- </el-option>
- </el-select>
- </el-form-item>
- <el-form-item label="发布状态:">
- <el-select placeholder="请选择" v-model="state">
- <el-option label="已发布" value="已发布"></el-option>
- <el-option label="草稿" value="草稿"></el-option>
- </el-select>
- </el-form-item>
- <el-form-item>
- <el-button type="primary">搜索</el-button>
- <el-button>重置</el-button>
- </el-form-item>
- </el-form>
- <!-- 文章列表 -->
- <el-table :data="articles" style="width: 100%">
- <el-table-column label="文章标题" width="400" prop="title"></el-table-column>
- <el-table-column label="分类" prop="categoryId"></el-table-column>
- <el-table-column label="发表时间" prop="createTime"> </el-table-column>
- <el-table-column label="状态" prop="state"></el-table-column>
- <el-table-column label="操作" width="100">
- <template #default="{ row }">
- <el-button :icon="Edit" circle plain type="primary"></el-button>
- <el-button :icon="Delete" circle plain type="danger"></el-button>
- </template>
- </el-table-column>
- <template #empty>
- <el-empty description="没有数据" />
- </template>
- </el-table>
- <!-- 分页条 -->
- <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :page-sizes="[3, 5 ,10, 15]"
- layout="jumper, total, sizes, prev, pager, next" background :total="total" @size-change="onSizeChange"
- @current-change="onCurrentChange" style="margin-top: 20px; justify-content: flex-end" />
- </el-card>
- </template>
- <style lang="scss" scoped>
- .page-container {
- min-height: 100%;
- box-sizing: border-box;
- .header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- }
- </style>
复制代码 使用中文语言包,办理分页条中文问题, 在main.js中完成
- import locale from 'element-plus/dist/locale/zh-cn.js'
- app.use(ElementPlus,{locale})
复制代码 16.2、文章分类数据回显
ArticleMange.vue
- //文章列表查询
- import { articleCategoryListService } from '@/api/article.js'
- const getArticleCategoryList = async () => {
- //获取所有分类
- let resultC = await articleCategoryListService();
- categorys.value = resultC.data
- }
- getArticleCategoryList();
复制代码
十七、 文章列表接口调用
article.js中提供获取文章列表数据的函数
- //文章列表查询
- export const articleListService = (params) => {
- return request.get('/article', { params: params })
- }
复制代码 ArticleManage.vue中,调用接口获取数据
- //文章列表查询
- import { articleListService } from '@/api/article.js'
- const getArticles = async () => {
- let params = {
- pageNum: pageNum.value,
- pageSize: pageSize.value,
- categoryId: categoryId.value ? categoryId.value : null,
- state: state.value ? state.value : null
- }
- let result = await articleListService(params);
- //渲染列表数据
- articles.value = result.data.items
- //为列表中添加categoryName属性
- for(let i=0;i<articles.value.length;i++){
- let article = articles.value[i];
- for(let j=0;j<categorys.value.length;j++){
- if(article.categoryId===categorys.value[j].id){
- article.categoryName=categorys.value[j].categoryName
- }
- }
- }
- //渲染总条数
- total.value=result.data.total
- }
- getArticles()
复制代码 当分页条的当前页和每页条数发生变化,重新发送请求获取数据
- //当每页条数发生了变化,调用此函数
- const onSizeChange = (size) => {
- pageSize.value = size
- getArticles()
- }
- //当前页码发生变化,调用此函数
- const onCurrentChange = (num) => {
- pageNum.value = num
- getArticles()
- }
复制代码 十八、搜刮和重置
为搜刮按钮绑定单击事故,调用getArticles函数即可
- <el-button type="primary" @click="getArticles">搜索</el-button>
复制代码 为重置按钮绑定单击事故,清除categoryId和state的之即可
- <el-button @click="categoryId='';state=''">重置</el-button>
复制代码 十九、添加文章
19.1 添加文章抽屉组件
- import {Plus} from '@element-plus/icons-vue'
- //控制抽屉是否显示
- const visibleDrawer = ref(false)
- //添加表单数据模型
- const articleModel = ref({
- title: '',
- categoryId: '',
- coverImg: '',
- content:'',
- state:''
- })
复制代码- <!-- 抽屉 -->
- <el-drawer v-model="visibleDrawer" title="添加文章" direction="rtl" size="50%">
- <!-- 添加文章表单 -->
- <el-form :model="articleModel" label-width="100px" >
- <el-form-item label="文章标题" >
- <el-input v-model="articleModel.title" placeholder="请输入标题"></el-input>
- </el-form-item>
- <el-form-item label="文章分类">
- <el-select placeholder="请选择" v-model="articleModel.categoryId">
- <el-option v-for="c in categorys" :key="c.id" :label="c.categoryName" :value="c.id">
- </el-option>
- </el-select>
- </el-form-item>
- <el-form-item label="文章封面">
- <el-upload class="avatar-uploader" :auto-upload="false" :show-file-list="false">
- <img v-if="articleModel.coverImg" :src="articleModel.coverImg" class="avatar" />
- <el-icon v-else class="avatar-uploader-icon">
- <Plus />
- </el-icon>
- </el-upload>
- </el-form-item>
- <el-form-item label="文章内容">
- <div class="editor">富文本编辑器</div>
- </el-form-item>
- <el-form-item>
- <el-button type="primary">发布</el-button>
- <el-button type="info">草稿</el-button>
- </el-form-item>
- </el-form>
- </el-drawer>
复制代码- /* 抽屉样式 */
- .avatar-uploader {
- :deep() {
- .avatar {
- width: 178px;
- height: 178px;
- display: block;
- }
- .el-upload {
- border: 1px dashed var(--el-border-color);
- border-radius: 6px;
- cursor: pointer;
- position: relative;
- overflow: hidden;
- transition: var(--el-transition-duration-fast);
- }
- .el-upload:hover {
- border-color: var(--el-color-primary);
- }
- .el-icon.avatar-uploader-icon {
- font-size: 28px;
- color: #8c939d;
- width: 178px;
- height: 178px;
- text-align: center;
- }
- }
- }
- .editor {
- width: 100%;
- :deep(.ql-editor) {
- min-height: 200px;
- }
- }
复制代码 为添加文章按钮添加单击事故,展示抽屉
- <el-button type="primary" @click="visibleDrawer = true">添加文章</el-button>
复制代码 19.2 富文本编辑器
文章内容需要使用到富文本编辑器,这里咱们使用一个开源的富文本编辑器 Quill
官网地址: https://vueup.github.io/vue-quill/
安装:
- npm install @vueup/vue-quill@latest --save
复制代码 导入组件和样式:
- import { QuillEditor } from '@vueup/vue-quill'
- import '@vueup/vue-quill/dist/vue-quill.snow.css'
复制代码 页面长使用quill组件:
- <quill-editor
- theme="snow"
- v-model:content="articleModel.content"
- contentType="html"
- >
- </quill-editor>
复制代码 样式美化:
- .editor {
- width: 100%;
- :deep(.ql-editor) {
- min-height: 200px;
- }
- }
复制代码 19.3 文章封面图片上传
将来当点击+图标,选择本地图片后,el-upload这个组件会主动发送请求,把图片上传到指定的服务器上,而不需要我们自己使用axios发送异步请求,所以需要给el-upload标签添加一些属性,控制请求的发送
auto-upload:是否主动上传
action: 服务器接口路径
name: 上传的文件字段名
headers: 设置上传的请求头
on-success: 上传乐成的回调函数
- import {
- Plus
- } from '@element-plus/icons-vue'
- <el-form-item label="文章封面">
- <el-upload class="avatar-uploader"
- :show-file-list="false"
- >
- <img v-if="articleModel.coverImg" :src="articleModel.coverImg" class="avatar" />
- <el-icon v-else class="avatar-uploader-icon">
- <Plus />
- </el-icon>
- </el-upload>
- </el-form-item>
复制代码 注意:
- 由于这个请求时el-upload主动发送的异步请求,并没有使用咱们的request.js请求工具,所以在请求的路ing上,需要加上/api, 这个时候请求署理才能拦截到这个请求,转发到后台服务器上
- 要携带请求头,还需要导入pinia状态才可以使用
- import { useTokenStore } from '@/stores/token.js'
- const tokenStore = useTokenStore();
复制代码 - 在乐成的回调函数中,可以拿到服务器相应的数据,其中有一个属性为data,对应的就是图片在阿里云oss上存储的访问地址,需要把它赋值给articleModel的coverImg属性,如许img标签就能显示这张图片了,由于img标签上通过src属性绑定了articleModel.coverImg
- //上传图片成功回调
- const uploadSuccess = (img) => {
- //img就是后台响应的数据,格式为:{code:状态码,message:提示信息,data: 图片的存储地址}
- articleModel.value.coverImg=img.data
- }
复制代码 19.4 添加文章接口调用
article.js中提供添加文章函数
- //添加文章
- export const articleAddService = (articleModel)=>{
- return request.post('/article',articleModel)
- }
复制代码 为已发布和草稿按钮绑定事故
- <el-form-item>
- <el-button type="primary" @click="addArticle('已发布')">发布</el-button>
- <el-button type="info" @click="addArticle('草稿')">草稿</el-button>
- </el-form-item>
复制代码 ArticleManage.vue中提供addArticle函数完成添加文章接口的调用
- //添加文章
- const addArticle=async (state)=>{
- articleModel.value.state = state
- let result = await articleAddService(articleModel.value);
- ElMessage.success(result.message? result.message:'添加成功')
- //再次调用getArticles,获取文章
- getArticles()
- //隐藏抽屉
- visibleDrawer.value=false
- }
复制代码 二十、顶部导航栏个人信息显示
在Layout.vue中,页面加载完就发送请求,获取个人信息展示,并存储到pinia中,由于将来在个人中心中修改信息的时候还需要使用
user.js中提供获取个人信息的函数
- //获取个人信息
- export const userInfoGetService = ()=>{
- return request.get('/user/userInfo');
- }
复制代码 src/stores/user.js中,定义个人中心状态
- import { defineStore } from "pinia"
- import {ref} from 'vue'
- export const useUserInfoStore = defineStore('userInfo',()=>{
- //1.定义用户信息
- const info = ref({})
- //2.定义修改用户信息的方法
- const setInfo = (newInfo)=>{
- info.value = newInfo
- }
- //3.定义清空用户信息的方法
- const removeInfo = ()=>{
- info.value={}
- }
- return{info,setInfo,removeInfo}
- },{
- persist:true
- })
复制代码 Layout.vue中获取个人信息,并存储到pinia中
- //导入接口函数
- import {userInfoGetService} from '@/api/user.js'
- //导入pinia
- import {useUserInfoStore} from '@/stores/user.js'
- const userInfoStore = useUserInfoStore();
- import {ref} from 'vue'
- //获取个人信息
- const getUserInf = async ()=>{
- let result = await userInfoGetService();
- //存储pinia
- userInfoStore.info =result.data;
- }
- getUserInf()
复制代码 Layout.vue的顶部导航栏中,展示昵称和头像
- <div>黑马程序员:<strong>{{ userInfoStore.info.nickname ? userInfoStore.info.nickname : userInfoStore.info.usrename }}</strong></div>
- <el-avatar :src="userInfoStore.info.userPic ? userInfoStore.info.userPic : avatar" />
复制代码 二十一、el-dropdown中功能实现
在el-dropdown中有四个子条目,分别是:
其中其三个起到路由功能,跟左侧菜单中【个人中心】下面的二级菜单是同样的功能,退出登录需要删除本地pinia中存储的token以及userInfo
路由实现:
在el-dropdown-item标签上添加command属性,属性值和路由表中/user/xxx保持一致
- <el-dropdown-menu>
- <el-dropdown-item command="info" :icon="User">基本资料</el-dropdown-item>
- <el-dropdown-item command="avatar" :icon="Crop">更换头像</el-dropdown-item>
- <el-dropdown-item command="password" :icon="EditPen">重置密码</el-dropdown-item>
- <el-dropdown-item command="logout" :icon="SwitchButton">退出登录</el-dropdown-item>
- </el-dropdown-menu>
复制代码 在el-dropdown标签上绑定command事故,当有条目被点击后,会触发这个事故
- <el-dropdown placement="bottom-end" @command="handleCommand">
复制代码 提供handleCommand函数,参数为点击条目标command属性值
- //dropDown条目被点击后,回调的函数
- import {useRouter} from 'vue-router'
- const router = useRouter()
- const handleCommand = (command)=>{
- if(command==='logout'){
- //退出登录
- alert('退出登录')
- }else{
- //路由
- router.push('/user/'+command)
- }
- }
复制代码 退出登录实现:
- import {ElMessage,ElMessageBox} from 'element-plus'
- import { useTokenStore } from '@/stores/token.js'
- const tokenStore = useTokenStore()
- const handleCommand = (command) => {
- if (command === 'logout') {
- //退出登录
- ElMessageBox.confirm(
- '你确认退出登录码?',
- '温馨提示',
- {
- confirmButtonText: '确认',
- cancelButtonText: '取消',
- type: 'warning',
- }
- )
- .then(async () => {
- //用户点击了确认
- //清空pinia中的token和个人信息
- userInfoStore.info={}
- tokenStore.token=''
- //跳转到登录页
- router.push('/login')
- })
- .catch(() => {
- //用户点击了取消
- ElMessage({
- type: 'info',
- message: '取消退出',
- })
- })
- } else {
- //路由
- router.push('/user/' + command)
- }
- }
复制代码 二十二、基本资料修改
22.1 基本资料页面组件
- <script setup>
- import { ref } from 'vue'
- const userInfo = ref({
- id: 0,
- username: 'zhangsan',
- nickname: 'zs',
- email: 'zs@163.com',
- })
- const rules = {
- nickname: [
- { required: true, message: '请输入用户昵称', trigger: 'blur' },
- {
- pattern: /^\S{2,10}$/,
- message: '昵称必须是2-10位的非空字符串',
- trigger: 'blur'
- }
- ],
- email: [
- { required: true, message: '请输入用户邮箱', trigger: 'blur' },
- { type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
- ]
- }
- </script>
- <template>
- <el-card class="page-container">
- <template #header>
- <div class="header">
- <span>基本资料</span>
- </div>
- </template>
- <el-row>
- <el-col :span="12">
- <el-form :model="userInfo" :rules="rules" label-width="100px" size="large">
- <el-form-item label="登录名称">
- <el-input v-model="userInfo.username" disabled></el-input>
- </el-form-item>
- <el-form-item label="用户昵称" prop="nickname">
- <el-input v-model="userInfo.nickname"></el-input>
- </el-form-item>
- <el-form-item label="用户邮箱" prop="email">
- <el-input v-model="userInfo.email"></el-input>
- </el-form-item>
- <el-form-item>
- <el-button type="primary">提交修改</el-button>
- </el-form-item>
- </el-form>
- </el-col>
- </el-row>
- </el-card>
- </template>
复制代码 22.2 表单数据回显
个人信息之前已经存储到了pinia中,只需要从pinia中获取个人信息,替换模板数据即可
- import { useUserInfoStore } from '@/stores/user.js';
- const userInfoStore = useUserInfoStore()
- const userInfo = ref({...userInfoStore.info})
复制代码 22.3 接口调用
在src/api/user.js中提供修改基本资料的函数
- //修改个人信息
- export const userInfoUpdateService = (userInfo)=>{
- return request.put('/user/update',userInfo)
- }
复制代码 为修改按钮绑定单击事故
- <el-button type="primary" @click="updateUserInfo">提交修改</el-button>
复制代码 提供updateUserInfo函数
- //修改用户信息
- import {userInfoUpdateService} from '@/api/user.js'
- import { ElMessage } from 'element-plus';
- const updateUserInfo = async ()=>{
- let result = await userInfoUpdateService(userInfo.value)
- ElMessage.success(result.message? result.message:'修改成功')
- //更新pinia中的数据
- userInfoStore.info.nickname=userInfo.value.nickname
- userInfoStore.info.email = userInfo.value.email
- }
复制代码 二十三、修改头像
23.1 修改头像页面组件
- <script setup>
- import { Plus, Upload } from '@element-plus/icons-vue'
- import {ref} from 'vue'
- import avatar from '@/assets/default.png'
- const uploadRef = ref()
- //用户头像地址
- const imgUrl= avatar
- </script>
- <template>
- <el-card class="page-container">
- <template #header>
- <div class="header">
- <span>更换头像</span>
- </div>
- </template>
- <el-row>
- <el-col :span="12">
- <el-upload
- ref="uploadRef"
- class="avatar-uploader"
- :show-file-list="false"
- >
- <img v-if="imgUrl" :src="imgUrl" class="avatar" />
- <img v-else src="avatar" width="278" />
- </el-upload>
- <br />
- <el-button type="primary" :icon="Plus" size="large" @click="uploadRef.$el.querySelector('input').click()">
- 选择图片
- </el-button>
- <el-button type="success" :icon="Upload" size="large">
- 上传头像
- </el-button>
- </el-col>
- </el-row>
- </el-card>
- </template>
- <style lang="scss" scoped>
- .avatar-uploader {
- :deep() {
- .avatar {
- width: 278px;
- height: 278px;
- display: block;
- }
- .el-upload {
- border: 1px dashed var(--el-border-color);
- border-radius: 6px;
- cursor: pointer;
- position: relative;
- overflow: hidden;
- transition: var(--el-transition-duration-fast);
- }
- .el-upload:hover {
- border-color: var(--el-color-primary);
- }
- .el-icon.avatar-uploader-icon {
- font-size: 28px;
- color: #8c939d;
- width: 278px;
- height: 278px;
- text-align: center;
- }
- }
- }
- </style>
复制代码 23.2 头像回显
从pinia中读取用户的头像数据
- //读取用户信息
- import {ref} from 'vue'
- import {useUserInfoStore} from '@/stores/user.js'
- const userInfoStore = useUserInfoStore()
- const imgUrl=ref(userInfoStore.info.userPic)
复制代码 img标签上绑定图片地址
- <img v-if="imgUrl" :src="imgUrl" class="avatar" />
- <img v-else src="@/assets/avatar.jpg" width="278" />
复制代码 23.3 头像上传
为el-upload指定属性值,分别有:
- action: 服务器接口路径
- headers: 设置请求头,需要携带token
- on-success: 上传乐成的回调函数
- name: 上传图片的字段名称
- <el-upload
- class="avatar-uploader"
- :show-file-list="false"
- :auto-upload="true"
- action="/api/upload"
- name="file"
- :headers="{'Authorization':tokenStore.token}"
- :on-success="uploadSuccess"
- >
- <img v-if="imgUrl" :src="imgUrl" class="avatar" />
- <img v-else src="@/assets/avatar.jpg" width="278" />
- </el-upload>
复制代码 提供上传乐成的回调函数
- //读取token信息
- import {useTokenStore} from '@/stores/token.js'
- const tokenStore = useTokenStore()
- //图片上传成功的回调
- const uploadSuccess = (result)=>{
- //回显图片
- imgUrl.value = result.data
- }
复制代码 外部触发图片选择
需要获取到el-upload组件,然后再通过$el.querySelector(‘input’)获取到el-upload对应的元素,触发click事故
- //获取el-upload元素
- const uploadRef = ref()
- <el-button type="primary" :icon="Plus" size="large" @click="uploadRef.$el.querySelector('input').click()">
- 选择图片
- </el-button>
复制代码 24.4 接口调用
在user.js中提供修改头像的函数
- //修改头像
- export const userAvatarUpdateService=(avatarUrl)=>{
- let params = new URLSearchParams();
- params.append('avatarUrl',avatarUrl)
- return request.patch('/user/updateAvatar',params)
- }
复制代码 为【上传头像】按钮绑定单击事故
- <el-button type="success" :icon="Upload" size="large" @click="updateAvatar">
- 上传头像
- </el-button>
复制代码 提供updateAvatar函数,完成头像更新
- //调用接口,更新头像url
- import {userAvatarUpdateService} from '@/api/user.js'
- import {ElMessage} from 'element-plus'
- const updateAvatar = async ()=>{
- let result = await userAvatarUpdateService(imgUrl.value)
- ElMessage.success(result.message? result.message:'修改成功')
- //更新pinia中的数据
- userInfoStore.info.userPic=imgUrl.value
- }
复制代码 前端源代码
一. api
article.js
- import request from '@/utils/request.js'//导入@/stores/token.jsimport { useTokenStore } from '@/stores/token.js'export const articleCategoryListService = () => { //获取token状态 // const tokenStore = useTokenStore() // //通过请求头Authorization携带token // return request.get('/category', { headers: { 'Authorization': tokenStore.token } }) return request.get('/category')}//添加文章分类
- export const articleCategoryAddService = (categoryModel) => {
- return request.post('/category', categoryModel)
- }
- //修改分类export const articleCategoryUpdateService = (categoryModel) => { return request.put('/category', categoryModel)}//删除分类export const articleCategoryDeleteService = (id) => { return request.delete('/category?id=' + id)}//文章列表查询
- export const articleListService = (params) => {
- return request.get('/article', { params: params })
- }
- //添加文章export const articleAddService = (articleModel) => { return request.post('/article', articleModel)}//修改文章export const articleEditService = (articleModel) => { return request.put('/article', articleModel)}//删除文章export const articleDeleteService = (id) => { return request.delete('/article?id=' + id)}
复制代码 user.js
- import request from '@/utils/request.js'
- export const userRegisterService = (registerData)=>{
- const params = new URLSearchParams()
- for (let key in registerData) {
- params.append(key, registerData[key])
- }
- return request.post('/user/register',params)
- }
- export const userLoginService = (loginData)=>{
- const params = new URLSearchParams()
- for (let key in loginData) {
- params.append(key, loginData[key])
- }
- return request.post('/user/login',params)
- }
- export const userInfoService = () => {
- return request.get('/user/userInfo')
- }
- //修改个人信息
- export const userInfoUpdateService = (userInfo) => {
- return request.put('/user/update', userInfo)
- }
- //修改头像
- export const userAvatarUpdateService = (avatarUrl) => {
- let params = new URLSearchParams();
- params.append('avatarUrl', avatarUrl)
- return request.patch('/user/updateAvatar', params)
- }
- //修改密码
- export const userUpdatePasswordService = (pwdModel) => {
- const params = {
- old_pwd: pwdModel.oldPwd,
- new_pwd: pwdModel.newPwd,
- re_pwd: pwdModel.rePwd
- };
- return request.patch('/user/updatePwd', params)
- }
复制代码 二. router
index.js
- //导入vue-router
- import { createRouter, createWebHistory } from 'vue-router'
- //导入组件
- import LoginVue from '@/views/Login.vue'
- import LayoutVue from '@/views/Layout.vue'
- import ArticleCategoryVue from '@/views/article/ArticleCategory.vue'
- import ArticleManageVue from '@/views/article/ArticleManage.vue'
- import UserAvatarVUe from '@/views/user/UserAvatar.vue'
- import UserInfoVue from '@/views/user/UserInfo.vue'
- import UserResetPasswordVue from '@/views/user/UserResetPassword.vue'
- //定义路由关系
- const routes = [
- { path: '/login', component: LoginVue },
- {
- path: '/', component: LayoutVue, children:[
- {path: '/article/category', component: ArticleCategoryVue },
- { path: '/article/manage', component: ArticleManageVue },
- { path: '/user/info', component: UserInfoVue },
- { path: '/user/avatar', component: UserAvatarVUe },
- { path: '/user/resetpassword', component: UserResetPasswordVue },
- ]
- }
- ]
- //创建路由器
- const router = createRouter({
- history: createWebHistory(),
- routes: routes
- });
- export default router
复制代码 三. stores
token.js
- import { defineStore } from "pinia";
- import { ref } from 'vue';
- /*
- defineStore参数描述:
- 第一个参数:给状态起名,具有唯一性
- 第二个参数:函数,可以把定义该状态中拥有的内容
- defineStore返回值描述:
- 返回的是一个函数,将来可以调用该函数,得到第二个参数中返回的内容
- */
- export const useTokenStore = defineStore('token', () => {
- //1.定义描述token
- const token = ref('')
- //2.定义修改token的方法
- const setToken = (newToken) => {
- token.value = newToken
- }
- //3.定义移除token的方法
- const removeToken = () => {
- token.value = ''
- }
- return {
- token, setToken, removeToken
- }
- }
- ,
- {
- persist: true
- }
- )
复制代码 userInfo.js
- import { defineStore } from "pinia"
- import { ref } from 'vue'
- export const useUserInfoStore = defineStore('userInfo', () => {
- //1.定义用户信息
- const info = ref({})
- //2.定义修改用户信息的方法
- const setInfo = (newInfo) => {
- info.value = newInfo
- }
- //3.定义清空用户信息的方法
- const removeInfo = () => {
- info.value = {}
- }
- return { info, setInfo, removeInfo }
- }, {
- persist: true
- })
复制代码 四. utils
request.js
- //定制请求的实例//导入axios npm install axios
- import axios from 'axios';import { ElMessage } from 'element-plus';//定义一个变量,记录公共的前缀 , baseURL// const baseURL = 'http://localhost:8080';const baseURL = '/api';const instance = axios.create({baseURL})//导入token状态import { useTokenStore } from '@/stores/token.js';import router from '@/router/index.js';//添加请求拦截器instance.interceptors.request.use( (config) => { //在发送请求之前做什么 let tokenStore = useTokenStore() //如果token中有值,在携带 if (tokenStore.token) { config.headers.Authorization = tokenStore.token } return config }, (err) => { //如果请求错误做什么 Promise.reject(err) })//添加相应拦截器instance.interceptors.response.use( result => { if (result.data.code === 0) { return result.data; } // alert(result.msg?result.msg:'服务异常') ElMessage.error(result.data.message ? result.data.message : '服务异常') return Promise.reject(result.data);//异步的状态转化成失败的状态 }, err => { //如果相应状态码时401,代表未登录,给出对应的提示,并跳转到登录页 if (err.response.status === 401) { ElMessage.error('请先登录!') router.push('/login') } else { ElMessage.error('服务异常'); } return Promise.reject(err);//异步的状态转化成失败的状态 })export default instance;
复制代码 五. views/article
ArticleCategory.vue
- <script setup>import { Edit, Delete} from '@element-plus/icons-vue'import { ref } from 'vue'const categorys = ref([ { "id": 3, "categoryName": "美食", "categoryAlias": "my", "createTime": "2023-09-02 12:06:59", "updateTime": "2023-09-02 12:06:59" }, { "id": 4, "categoryName": "娱乐", "categoryAlias": "yl", "createTime": "2023-09-02 12:08:16", "updateTime": "2023-09-02 12:08:16" }, { "id": 5, "categoryName": "军事", "categoryAlias": "js", "createTime": "2023-09-02 12:08:33", "updateTime": "2023-09-02 12:08:33" }])import {articleCategoryListService, articleCategoryAddService, articleCategoryUpdateService,articleCategoryDeleteService} from '@/api/article.js'const articleCategoryList = async() => { let result = await articleCategoryListService(); categorys.value = result.data;}articleCategoryList();//控制添加分类弹窗
- const dialogVisible = ref(false)
- //添加分类数据模型
- const categoryModel = ref({
- categoryName: '',
- categoryAlias: ''
- })
- //添加分类表单校验
- const rules = {
- categoryName: [
- { required: true, message: '请输入分类名称', trigger: 'blur' },
- ],
- categoryAlias: [
- { required: true, message: '请输入分类别名', trigger: 'blur' },
- ]
- }
- import { ElMessage } from 'element-plus'const addCategory = async() => { let result = await articleCategoryAddService(categoryModel.value); ElMessage.success(result.message?result.message:'添加乐成'); articleCategoryList(); dialogVisible.value = false;}const title = ref('')const showDialog = (row) => { dialogVisible.value = true; title.value = '编辑分类' categoryModel.value.categoryName = row.categoryName categoryModel.value.categoryAlias = row.categoryAlias categoryModel.value.id = row.id}//修改分类const updateCategory = async () => { let result = await articleCategoryUpdateService(categoryModel.value) ElMessage.success(result.message ? result.message : '修改乐成') //隐藏弹窗 dialogVisible.value = false //再次访问后台接口,查询所有分类 articleCategoryList()}//清空模型数据const clearCategoryModel = () => { categoryModel.value.categoryName = '', categoryModel.value.categoryAlias = ''}//删除分类 给删除按钮绑定事故import { ElMessageBox } from 'element-plus'//删除分类const deleteCategory = (row) => { ElMessageBox.confirm( '你确认删除该分类信息吗?', '温馨提示', { confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning', } ) .then(async () => { //用户点击了确认 let result = await articleCategoryDeleteService(row.id) ElMessage.success(result.message ? result.message : '删除乐成') //再次调用getAllCategory,获取所有文章分类 articleCategoryList() }) .catch(() => { //用户点击了取消 ElMessage({ type: 'info', message: '取消删除', }) })}</script><template> <el-card class="page-container"> <template #header> <div class="header"> <span>文章分类</span> <div class="extra"> <el-button type="primary" @click="title = '添加分类'; dialogVisible = true; clearCategoryModel()">添加分类</el-button> </div> </div> </template> <el-table :data="categorys" style="width: 100%"> <el-table-column label="序号" width="100" type="index"> </el-table-column> <el-table-column label="分类名称" prop="categoryName"></el-table-column> <el-table-column label="分类别名" prop="categoryAlias"></el-table-column> <el-table-column label="操作" width="100"> <template #default="{ row }"> <el-button :icon="Edit" circle plain type="primary" @click="showDialog(row)"></el-button> <el-button :icon="Delete" circle plain type="danger" @click="deleteCategory(row)"></el-button> </template> </el-table-column> <template #empty> <el-empty description="没有数据" /> </template> </el-table> <!-- 添加分类弹窗 --> <el-dialog v-model="dialogVisible" :title="title" width="30%">
- <el-form :model="categoryModel" :rules="rules" label-width="100px" style="padding-right: 30px"> <el-form-item label="分类名称" prop="categoryName"> <el-input v-model="categoryModel.categoryName" minlength="1" maxlength="10"></el-input> </el-form-item> <el-form-item label="分类别名" prop="categoryAlias"> <el-input v-model="categoryModel.categoryAlias" minlength="1" maxlength="15"></el-input> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="title==='添加分类'?addCategory():updateCategory()"> 确认 </el-button> </span> </template> </el-dialog> </el-card></template><style lang="scss" scoped>.page-container { min-height: 100%; box-sizing: border-box; .header { display: flex; align-items: center; justify-content: space-between; }}</style>
复制代码 ArticleManage.vue
- <script setup>import { Edit, Delete} from '@element-plus/icons-vue'import { ref } from 'vue'//文章分类数据模型const categorys = ref([ { "id": 3, "categoryName": "美食", "categoryAlias": "my", "createTime": "2023-09-02 12:06:59", "updateTime": "2023-09-02 12:06:59" }, { "id": 4, "categoryName": "娱乐", "categoryAlias": "yl", "createTime": "2023-09-02 12:08:16", "updateTime": "2023-09-02 12:08:16" }, { "id": 5, "categoryName": "军事", "categoryAlias": "js", "createTime": "2023-09-02 12:08:33", "updateTime": "2023-09-02 12:08:33" }])//用户搜刮时选中的分类idconst categoryId = ref('')//用户搜刮时选中的发布状态const state = ref('')//文章列表数据模型const articles = ref([ { "id": 5, "title": "陕西旅游攻略", "content": "戎马俑,华清池,法门寺,西岳...爱去哪去哪...", "coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png", "state": "草稿", "categoryId": 2, "createTime": "2023-09-03 11:55:30", "updateTime": "2023-09-03 11:55:30" }, { "id": 5, "title": "陕西旅游攻略", "content": "戎马俑,华清池,法门寺,西岳...爱去哪去哪...", "coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png", "state": "草稿", "categoryId": 2, "createTime": "2023-09-03 11:55:30", "updateTime": "2023-09-03 11:55:30" }, { "id": 5, "title": "陕西旅游攻略", "content": "戎马俑,华清池,法门寺,西岳...爱去哪去哪...", "coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png", "state": "草稿", "categoryId": 2, "createTime": "2023-09-03 11:55:30", "updateTime": "2023-09-03 11:55:30" },])//分页条数据模型const pageNum = ref(1)//当前页const total = ref(20)//总条数const pageSize = ref(3)//每页条数//当每页条数发生了变化,调用此函数const onSizeChange = (size) => { pageSize.value = size articleList()}//当前页码发生变化,调用此函数const onCurrentChange = (num) => { pageNum.value = num articleList()}//文章列表查询import { articleCategoryListService, articleListService, articleAddService, articleEditService, articleDeleteService } from '@/api/article.js'const articleCategoryList = async () => { //获取所有分类 let resultC = await articleCategoryListService(); categorys.value = resultC.data}const articleList = async () => { let params = { pageNum: pageNum.value, pageSize: pageSize.value, categoryId: categoryId.value ? categoryId.value : null, state: state.value ? state.value : null } let result = await articleListService(params); //渲染总条数 total.value = result.data.total //渲染列表数据 articles.value = result.data.items //为列表中添加categoryName属性 for (let i = 0; i < articles.value.length; i++) { let article = articles.value[i]; for (let j = 0; j < categorys.value.length; j++) { if (article.categoryId === categorys.value[j].id) { article.categoryName = categorys.value[j].categoryName } } }}articleCategoryList();articleList();import { QuillEditor } from '@vueup/vue-quill'
- import '@vueup/vue-quill/dist/vue-quill.snow.css'
- import { Plus } from '@element-plus/icons-vue'//控制抽屉是否显示const visibleDrawer = ref(false)//添加表单数据模型const articleModel = ref({ title: '', categoryId: '', coverImg: '', content: '', state: ''})import { useTokenStore } from '@/stores/token.js';const tokenStore = useTokenStore();//上传图片乐成回调const uploadSuccess = (result) => { //img就是后台相应的数据,格式为:{code:状态码,message:提示信息,data: 图片的存储地址} articleModel.value.coverImg = result.data console.log(result.data)}const title = ref('新增文章')//添加文章import { ElMessage } from 'element-plus'const addArticle = async (state) => { articleModel.value.state = state let result = await articleAddService(articleModel.value); ElMessage.success(result.message ? result.message : '添加乐成') //隐藏抽屉 visibleDrawer.value = false //再次调用articleList方法获取文章 articleList();}const showForm = (row) => { title.value = '编辑文章' //将文章数据渲染到表单中 articleModel.value.title = row.title articleModel.value.categoryId = row.categoryId articleModel.value.coverImg = row.coverImg articleModel.value.content = row.content articleModel.value.state = row.state // 确保编辑器的内容也渲染 articleModel.value.id = row.id}const editArticle = async (state) => { articleModel.value.state = state let result = await articleEditService(articleModel.value) ElMessage.success('修改乐成') //隐藏抽屉 visibleDrawer.value = false //再次调用articleList方法获取文章 articleList();}const clearArticelForm = () => { title.value = '新增文章' articleModel.value.title = ''; articleModel.value.categoryId = ''; articleModel.value.coverImg = ''; articleModel.value.content = ''; articleModel.value.state = ''; // 确保编辑器的内容也清空 if (document.querySelector('.ql-editor')) { document.querySelector('.ql-editor').innerHTML = ''; // 清空编辑器的内容 }}const deleteArticle = async (row) => { let result = await articleDeleteService(row.id) ElMessage.success(result.message ? result.message : '删除乐成') articleList()}</script><template> <el-card class="page-container"> <template #header> <div class="header"> <span>文章管理</span> <div class="extra"> <el-button type="primary" @click="visibleDrawer = true; clearArticelForm()">添加文章</el-button> </div> </div> </template> <!-- 搜刮表单 --> <el-form inline> <el-form-item label=" 文章分类:"> <el-select placeholder="请选择" v-model="categoryId" style="width: 200px;"> <el-option v-for="c in categorys" :key="c.id" :label="c.categoryName" :value="c.id"> </el-option> </el-select> </el-form-item> <el-form-item label="发布状态:"> <el-select placeholder="请选择" v-model="state" style="width: 200px;"> <el-option label=" 已发布" value="已发布"></el-option> <el-option label="草稿" value="草稿"></el-option> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="articleList">搜刮</el-button> <el-button @click="categoryId = ''; state = ''; articleList()">重置</el-button> </el-form-item> </el-form> <!-- 文章列表 --> <el-table :data="articles" style="width: 100%"> <el-table-column label="文章标题" width="400" prop="title"></el-table-column> <el-table-column label="分类" prop="categoryName"></el-table-column> <el-table-column label="发表时间" prop="createTime"> </el-table-column> <el-table-column label="状态" prop="state"></el-table-column> <el-table-column label="操作" width="100"> <template #default="{ row }"> <el-button :icon="Edit" circle plain type="primary" @click="visibleDrawer = true; showForm(row);"></el-button> <el-button :icon="Delete" circle plain type="danger" @click="deleteArticle(row)"></el-button> </template> </el-table-column> <template #empty> <el-empty description="没有数据" /> </template> </el-table> <!-- 分页条 --> <el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :page-sizes="[3, 5, 10, 15]" layout="jumper, total, sizes, prev, pager, next" background :total="total" @size-change="onSizeChange" @current-change="onCurrentChange" style="margin-top: 20px; justify-content: flex-end" /> <!-- 抽屉 --> <el-drawer v-model="visibleDrawer" :title="title" direction="rtl" size="50%"> <!-- 添加文章表单 --> <el-form :model="articleModel" label-width="100px"> <el-form-item label="文章标题"> <el-input v-model="articleModel.title" placeholder="请输入标题"></el-input> </el-form-item> <el-form-item label="文章分类"> <el-select placeholder="请选择" v-model="articleModel.categoryId"> <el-option v-for="c in categorys" :key="c.id" :label="c.categoryName" :value="c.id"> </el-option> </el-select> </el-form-item> <el-form-item label="文章封面"> <el-upload class="avatar-uploader" :auto-upload="true" :show-file-list="false" action="/api/upload" name="file" :headers="{ 'Authorization': tokenStore.token }" :on-success="uploadSuccess"> <img v-if="articleModel.coverImg" :src="articleModel.coverImg" class="avatar" /> <el-icon v-else class="avatar-uploader-icon"> <Plus /> </el-icon> </el-upload> </el-form-item> <el-form-item label="文章内容"> <div class="editor"> <quill-editor theme="snow" v-model:content="articleModel.content" contentType="html"> </quill-editor> </div> </el-form-item> <el-form-item> <el-button type="primary" @click="title === '编辑文章' ? editArticle('已发布') : addArticle('已发布')">发布</el-button> <el-button type="info" @click="title === '编辑文章' ? editArticle('草稿') : addArticle('草稿')">草稿</el-button> </el-form-item> </el-form> </el-drawer> </el-card></template><style lang="scss" scoped>.page-container { min-height: 100%; box-sizing: border-box; .header { display: flex; align-items: center; justify-content: space-between; }}/* 抽屉样式 */.avatar-uploader { :deep() { .avatar { width: 178px; height: 178px; display: block; } .el-upload { border: 1px dashed var(--el-border-color); border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; transition: var(--el-transition-duration-fast); } .el-upload:hover { border-color: var(--el-color-primary); } .el-icon.avatar-uploader-icon { font-size: 28px; color: #8c939d; width: 178px; height: 178px; text-align: center; } }}.editor { width: 100%; :deep(.ql-editor) { min-height: 200px; }}</style>
复制代码 六. views/user
UserAvatar.vue
- <script setup>
- import { Plus, Upload } from '@element-plus/icons-vue'
- import { ref } from 'vue'
- import avatar from '@/assets/default.png'
- const uploadRef = ref()
- import { useTokenStore } from '@/stores/token.js'
- const tokenStore = useTokenStore()
- import { useUserInfoStore } from '@/stores/userInfo.js'
- const userInfoStore = useUserInfoStore()
- //用户头像地址
- const imgUrl = ref(userInfoStore.info.userPic)
- const uploadSuccess = (result) => {
- imgUrl.value = result.data
- }
- //调用接口,更新头像url
- import { userAvatarUpdateService } from '@/api/user.js'
- import { ElMessage } from 'element-plus'
- const updateAvatar = async () => {
- let result = await userAvatarUpdateService(imgUrl.value)
- ElMessage.success(result.message ? result.message : '修改成功')
- //更新pinia中的数据
- userInfoStore.info.userPic = imgUrl.value
- }
- </script>
- <template>
- <el-card class="page-container">
- <template #header>
- <div class="header">
- <span>更换头像</span>
- </div>
- </template>
- <el-row>
- <el-col :span="12">
- <el-upload ref="uploadRef" class="avatar-uploader" :show-file-list="false" :auto-upload="true"
- action="/api/upload" name="file" :headers="{'Authorization': tokenStore.token}"
- :on-success="uploadSuccess">
- <img v-if="imgUrl" :src="imgUrl" class="avatar" />
- <img v-else :src="avatar" width="278" />
- </el-upload>
- <br />
- <el-button type="primary" :icon="Plus" size="large"
- @click="uploadRef.$el.querySelector('input').click()">
- 选择图片
- </el-button>
- <el-button type="success" :icon="Upload" size="large" @click="updateAvatar">
- 上传头像
- </el-button>
- </el-col>
- </el-row>
- </el-card>
- </template>
- <style lang="scss" scoped>
- .avatar-uploader {
- :deep() {
- .avatar {
- width: 278px;
- height: 278px;
- display: block;
- }
- .el-upload {
- border: 1px dashed var(--el-border-color);
- border-radius: 6px;
- cursor: pointer;
- position: relative;
- overflow: hidden;
- transition: var(--el-transition-duration-fast);
- }
- .el-upload:hover {
- border-color: var(--el-color-primary);
- }
- .el-icon.avatar-uploader-icon {
- font-size: 28px;
- color: #8c939d;
- width: 278px;
- height: 278px;
- text-align: center;
- }
- }
- }
- </style>
复制代码 UserInfo.vue
- <script setup>import { ref } from 'vue'import { useUserInfoStore } from '@/stores/userInfo.js';const userInfoStore = useUserInfoStore()const userInfo = ref({ ...userInfoStore.info })const rules = { nickname: [ { required: true, message: '请输入用户昵称', trigger: 'blur' }, { pattern: /^\S{2,10}$/, message: '昵称必须是2-10位的非空字符串', trigger: 'blur' } ], email: [ { required: true, message: '请输入用户邮箱', trigger: 'blur' }, { type: 'email', message: '邮箱格式不精确', trigger: 'blur' } ]}import { userInfoUpdateService } from '@/api/user.js'import { ElMessage } from 'element-plus'//修改个人信息const updateUserInfo = async() => { let result = await userInfoUpdateService(userInfo.value) ElMessage.success(result.message ? result.message : '修改乐成') //修改pinia用户信息 userInfoStore.setInfo(userInfo.value)}</script><template> <el-card class="page-container"> <template #header> <div class="header"> <span>基本资料</span> </div> </template> <el-row> <el-col :span="12"> <el-form :model="userInfo" :rules="rules" label-width="100px" size="large"> <el-form-item label="登录名称"> <el-input v-model="userInfo.username" disabled></el-input> </el-form-item> <el-form-item label="用户昵称" prop="nickname"> <el-input v-model="userInfo.nickname"></el-input> </el-form-item> <el-form-item label="用户邮箱" prop="email"> <el-input v-model="userInfo.email"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="updateUserInfo">提交修改</el-button>
- </el-form-item> </el-form> </el-col> </el-row> </el-card></template>
复制代码 UserResetPassword.vue
- <template>
- <el-card >
- <el-form style="max-width: 600px" :model="pwdModel" status-icon :rules="rules" label-width="auto"
- class="demo-ruleForm">
- <el-form-item label="原密码" prop="oldPwd">
- <el-input v-model="pwdModel.oldPwd" type="password" autocomplete="off" />
- </el-form-item>
- <el-form-item label="新密码" prop="newPwd">
- <el-input v-model="pwdModel.newPwd" type="password" autocomplete="off" />
- </el-form-item>
- <el-form-item label="确认新密码" prop="rePwd">
- <el-input v-model="pwdModel.rePwd" type="password" autocomplete="off" />
- </el-form-item>
- <el-form-item>
- <el-button type="primary" @click="submitForm()" style="margin-left: 30%;">
- 确认
- </el-button>
- <el-button @click="resetForm()">重置</el-button>
- </el-form-item>
- </el-form>
- </el-card>
- </template>
- <script lang="ts" setup>
- import {ref } from 'vue'
- const validateRePwd = (rule, value, callback) => {
- if (value === '') {
- callback(new Error('请再次输入新密码'))
- } else if (value !== pwdModel.value.newPwd) {
- callback(new Error("确认密码与新密码不匹配"))
- } else {
- callback()
- }
- }
- const pwdModel= ref({
- oldPwd: '',
- newPwd: '',
- rePwd: '',
- })
- const rules = {
- oldPwd: [{ required: true, message: '请输入原密码', trigger: 'blur' },
- { pattern: /^\S{6,16}$/, message: '密码长度必须是6-16位的非空字符串', trigger: 'blur' }
-
- ],
- newPwd: [{ required: true, message: '请输入新密码', trigger: 'blur' },
- { pattern: /^\S{6,16}$/, message: '密码长度必须是6-16位的非空字符串', trigger: 'blur' }
- ],
- rePwd: [{ validator: validateRePwd, trigger: 'blur' },
- { pattern: /^\S{6,16}$/, message: '密码长度必须是6-16位的非空字符串', trigger: 'blur' }
- ],
- }
- import { ElMessage } from 'element-plus'
- import { useUserInfoStore } from '@/stores/userInfo.js'
- import { useRouter } from 'vue-router';
- const router = useRouter()
- const userInfoStore = useUserInfoStore()
- import { userUpdatePasswordService } from '@/api/user.js'
- const submitForm = async () => {
-
- let result = await userUpdatePasswordService(pwdModel.value)
- ElMessage.success('修改成功,即将跳转登录页')
- //修改pinia用户信息
- userInfoStore.setInfo(pwdModel.value)
- resetForm()
- setTimeout(() => {
- router.push('/login')
- }, 2000);
-
- }
- const resetForm = () => {
- pwdModel.value = {
- oldPwd: '',
- newPwd: '',
- rePwd: ''
- }
- }
- </script>
复制代码 七. Layout.vue
- <script setup>import { Management, Promotion, UserFilled, User, Crop, EditPen, SwitchButton, CaretBottom} from '@element-plus/icons-vue'import avatar from '@/assets/default.png'import { userInfoService } from '@/api/user.js'import { useUserInfoStore } from '@/stores/userInfo';import { useTokenStore } from '@/stores/token';const TokenStore= useTokenStore();const userInfoStore = useUserInfoStore();//获取用户详细信息const getUserInfo = async() => { let result = await userInfoService(); userInfoStore.setInfo(result.data);}getUserInfo();import { ElMessageBox, ElMessage } from 'element-plus';import { useRouter } from 'vue-router'
- const router = useRouter();
- const handleCommand = (command) => { if (command === 'logout') { ElMessageBox.confirm( '你确认退出登录吗?', '温馨提示', { confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning', } ) .then(async () => { TokenStore.removeToken(); userInfoStore.removeInfo(); router.push('/login'); //用户点击了确认 ElMessage.success('退出登录乐成') }) .catch(() => { //用户点击了取消 ElMessage({ type: 'info', message: '取消登录', }) }) } else { router.push(`/user/${command}`); }}</script><template> <el-container class="layout-container"> <!-- 左侧菜单 --> <el-aside width="200px"> <div class="el-aside__logo"></div> <el-menu active-text-color="#ffd04b" background-color="#232323" text-color="#fff" router> <el-menu-item index="/article/category"> <el-icon> <Management /> </el-icon> <span>文章分类</span> </el-menu-item> <el-menu-item index="/article/manage"> <el-icon> <Promotion /> </el-icon> <span>文章管理</span> </el-menu-item> <el-sub-menu> <template #title> <el-icon> <UserFilled /> </el-icon> <span>个人中心</span> </template> <el-menu-item index="/user/info"> <el-icon> <User /> </el-icon> <span>基本资料</span> </el-menu-item> <el-menu-item index="/user/avatar"> <el-icon> <Crop /> </el-icon> <span>更换头像</span> </el-menu-item> <el-menu-item index="/user/resetpassword"> <el-icon> <EditPen /> </el-icon> <span>重置密码</span> </el-menu-item> </el-sub-menu> </el-menu> </el-aside> <!-- 右侧主地区 --> <el-container> <!-- 头部地区 --> <el-header> <div>黑马程序员:<strong>{{ userInfoStore.info.nickname }}</strong></div> <el-dropdown placement="bottom-end" @command="handleCommand">
- <span class="el-dropdown__box"> <el-avatar :src="userInfoStore.info.userPic? userInfoStore.info.userPic : avatar" /> <el-icon> <CaretBottom /> </el-icon> </span> <template #dropdown> <el-dropdown-menu> <el-dropdown-item command="info" :icon="User">基本资料</el-dropdown-item> <el-dropdown-item command="avatar" :icon="Crop">更换头像</el-dropdown-item> <el-dropdown-item command="resetpassword" :icon="EditPen">重置密码</el-dropdown-item> <el-dropdown-item command="logout" :icon="SwitchButton">退出登录</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </el-header> <!-- 中间地区 --> <el-main> <!-- <div style="width: 1290px; height: 570px;border: 1px solid red;"> 内容展示区 </div> --> <router-view></router-view> </el-main> <!-- 底部地区 --> <el-footer>大事故 ©2023 Created by 黑马程序员</el-footer> </el-container> </el-container></template><style lang="scss" scoped>.layout-container { height: 100vh; .el-aside { background-color: #232323; &__logo { height: 120px; background: url('@/assets/logo.png') no-repeat center / 120px auto; } .el-menu { border-right: none; } } .el-header { background-color: #fff; display: flex; align-items: center; justify-content: space-between; .el-dropdown__box { display: flex; align-items: center; .el-icon { color: #999; margin-left: 10px; } &:active, &:focus { outline: none; } } } .el-footer { display: flex; align-items: center; justify-content: center; font-size: 14px; color: #666; }}</style>
复制代码 八. Login,vue
- <script setup>import { User, Lock } from '@element-plus/icons-vue'import { ref } from 'vue'//控制注册与登录表单的显示, 默认显示注册const isRegister = ref(false)const registerData = ref({ username: '', password: '', rePassword: ''})//自定义确认密码的校验函数const rePasswordValid = (rule, value, callback) => { if (value == null || value === '') { return callback(new Error('请再次确认密码')) } if (registerData.value.password !== value) { return callback(new Error('两次输入密码不一致')) }}//用于注册的表单校验模型const registerDataRules = { username: [ { required: true, message: '请输入用户名', trigger: 'blur' }, { min: 5, max: 16, message: '用户名的长度必须为5~16位', trigger: 'blur' } ], password: [ { required: true, message: '请输入密码', trigger: 'blur' }, { min: 5, max: 16, message: '密码长度必须为5~16位', trigger: 'blur' } ], rePassword: [ { validator: rePasswordValid, trigger: 'blur' } ]}//用于注册的事故函数import { userRegisterService, userLoginService } from '@/api/user.js';import { ElMessage } from 'element-plus';const register = async () => { let result = await userRegisterService(registerData.value) // if (result.code === 0) { // alert(result.msg?result.msg:'注册乐成') // }else{ // alert(result.msg?result.msg:'注册失败') // } // alert(result.msg ? result.msg : '注册乐成') ElMessage.success(result.message ? result.message : '注册乐成') }import { useTokenStore } from '@/stores/token.js'import { useRouter } from 'vue-router'//调用useTokenStore得到状态const tokenStore = useTokenStore();const router = useRouter();//用于登陆事故函数const login = async () => { let result = await userLoginService(registerData.value) //生存token tokenStore.setToken(result.data) // if (result.code === 0) { // alert(result.msg?result.msg:'登录乐成') // }else{ // alert(result.msg?result.msg:'登录失败') // } // alert(result.msg ? result.msg : '登录乐成') ElMessage.success('登录乐成') router.push('/')
- }const clearRegisterData = () => { registerData.value = { username: '', password: '', rePassword: '' }}</script><template> <el-row class="login-page"> <el-col :span="12" class="bg"></el-col> <el-col :span="6" :offset="3" class="form"> <!-- 注册表单 --> <el-form ref="form" size="large" autocomplete="off" v-if="isRegister" :model="registerData" :rules="registerDataRules"> <el-form-item> <h1>注册</h1> </el-form-item> <el-form-item prop="username"> <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input> </el-form-item> <el-form-item prop="password"> <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input> </el-form-item> <el-form-item prop="rePassword"> <el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码" v-model="registerData.rePassword"></el-input> </el-form-item> <!-- 注册按钮 --> <el-form-item> <el-button @click="register(registerData)" class="button" type="primary" auto-insert-space> 注册 </el-button> </el-form-item> <el-form-item class="flex"> <el-link type="info" :underline="false" @click="isRegister = false; clearRegisterData()"> ← 返回 </el-link> </el-form-item> </el-form> <!-- 登录表单 --> <el-form ref="form" size="large" autocomplete="off" v-else :model="registerData" :rules="registerDataRules"> <el-form-item> <h1>登录</h1> </el-form-item> <el-form-item prop="username"> <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input> </el-form-item> <el-form-item prop="password"> <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input> </el-form-item> <el-form-item class="flex"> <div class="flex"> <el-checkbox>记着我</el-checkbox> <el-link type="primary" :underline="false">忘记密码?</el-link> </div> </el-form-item> <!-- 登录按钮 --> <el-form-item> <el-button class="button" type="primary" auto-insert-space @click="login(registerData)">登录</el-button> </el-form-item> <el-form-item class="flex"> <el-link type="info" :underline="false" @click="isRegister = true; clearRegisterData()"> 注册 → </el-link> </el-form-item> </el-form> </el-col> </el-row></template><style lang="scss" scoped>/* 样式 */.login-page { height: 100vh; background-color: #fff; .bg { background: url('@/assets/logo2.png') no-repeat 60% center / 240px auto, url('@/assets/login_bg.jpg') no-repeat center / cover; border-radius: 0 20px 20px 0; } .form { display: flex; flex-direction: column; justify-content: center; user-select: none; .title { margin: 0 auto; } .button { width: 100%; } .flex { width: 100%; display: flex; justify-content: space-between; } }}</style>
复制代码 九. App.vue
- <script setup>
- </script>
- <template>
- <RouterView></RouterView>
- </template>
复制代码 十. main.js
- import { createApp } from 'vue'
- import ElementPlus from 'element-plus'
- import 'element-plus/dist/index.css'
- import locale from 'element-plus/dist/locale/zh-cn.js'
- import App from './App.vue'
- import router from '@/router'
- import { createPinia } from 'pinia'
- //导入持久化插件
- import { createPersistedState } from 'pinia-persistedstate-plugin'
- const app = createApp(App)
- const pinia = createPinia()
- const persist = createPersistedState()
- //pinia使用持久化插件
- pinia.use(persist)
- app.use(pinia)
- app.use(ElementPlus, { locale })
- app.use(router)
- app.mount('#app')
复制代码 前端源代码GitHub堆栈:点击检察前端源代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |