Java 小项目开发日记 06(Vue3 前端开发)
一、环境准备
1.1 创建vue工程(big-event-admin)
npm init vue@latest
cd big-event-admin
npm install
1.2 安装插件
1. 安装element-plus
cnpm i element-plus --save
2. 安装axios
cnpm i axios
3. 安装sass依赖
cnpm i sass -D
4. 安装路由
cnpm install vue-router
5. 安装 pinia 插件
Pinia是Vue的专属状态管理库,它允许你跨组件或页面共享状态
cnpm install pinia
6. 安装 persist 插件
默认情况下,由于pinia是内存存储,当你刷新页面的时候pinia中的数据会丢失,可以借助于persist插件解决这个问题,persist插件支持将pinia中的数据持久化到sessionStorage和localStorage中
cnpm install pinia-persistedstate-plugin
二、vite.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': { // 获取请求中带 /api 的请求
target: 'http://localhost:8080', // 后台服务器的源
changeOrigin: true, // 修改源
rewrite: (path) => path.replace(/^\/api/, "") // /api 替换为空字符串
}
}
}
})
三、main.js
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from '@/router';
import {createPinia} from 'pinia'
import { createPersistedState } from 'pinia-persistedstate-plugin';
// 创建 app 实例
const app = createApp(App);
// 创建 pinia 实例
const pinia = createPinia();
// 创建 pinia 插件
const persist = createPersistedState()
// 使用 pinia 插件
pinia.use(persist)
// 挂载 ElementPlus
app.use(router).use(pinia).use(ElementPlus);
app.mount('#app');
四、 src目录
api
article.js
// 导入 request.js 请求工具
import request from '@/utils/request.js';
// 获取登录用户的全部文章
export const getMySelfAllCategoryService = () => {
return request.get('/category');
}
// 添加文章分类
export const addCategoryService = (categoryData) => {
return request.post('/category', categoryData);
}
user.js
// 导入 request.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);
}
router
index.js
// 导入 vue-router
import { createRouter, createWebHistory } from 'vue-router'
// 导入组件
import Login from "@/views/login/Login.vue";
import Main from "@/views/main/Main.vue";
import ArticleCategory from "@/views/article/ArticleCategory.vue";
import ArticleManage from "@/views/article/ArticleManage.vue";
import UserAvatar from "@/views/user/UserAvatar.vue";
import UserInfo from "@/views/user/UserInfo.vue";
import UserResetPassword from "@/views/user/UserResetPassword.vue";
// 定义路由关系
const routes = [
{ path: '/login', component: Login },
{
path: '/', component: Main,
// 子路由
children: [
{path: '/article/category', component: ArticleCategory},
{path: '/article/manage', component: ArticleManage},
{path: '/user/info', component: UserInfo},
{path: '/user/avatar', component: UserAvatar},
{path: '/user/password', component: UserResetPassword},
],
redirect: '/article/category'
},
]
// 创建路由
const router = createRouter({
history: createWebHistory(),
routes: routes
});
export default router;
store
token.js
import { defineStore } from 'pinia';
import { ref } from 'vue'
/**
* @description:
* @param {*} token 名字需要唯一性
* @param {*} 函数 函数的内部可以定义状态的所有内容
* @return {*}
*/
export const useTokenStore = defineStore('token', () => {
// 描述 token
const token = ref('');
// 定义修改 token 的方法
const setToken = (newToken) => {
token.value = newToken;
}
// 定义移除 token 的方法
const removeToken = () => {
token.value = '';
}
return { token, setToken, removeToken }
},
{
persist: true
});
utils
request.js
/*
* @Date: 2024-03-03 16:45:08
* @LastEditors: zhong
* @LastEditTime: 2024-03-04 20:04:17
* @FilePath: \big-event-vue\big-event-admin\src\utils\request.js
*/
// 导入 axios 依赖
import router from '@/router';
import { useTokenStore } from '@/store/token.js';
import axios from 'axios';
import { ElMessage } from 'element-plus'
// 定义baseUrl
const baseURL = '/api';
// 创建实例
const instance = axios.create({
baseURL: baseURL,
});
// 添加请求拦截
instance.interceptors.request.use(
(config) => {
// 请求成功操作
let tokenStore = useTokenStore();
if (tokenStore.token) {
config.headers.Authorization = tokenStore.token;
}
return config;
},
(err) => {
// 请求错误操作
return Promise.reject(err);
}
)
// 添加响应拦截器
instance.interceptors.response.use(
result => {
if (result.data.code === 0) {
return result.data;
}
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
<!--
* @Date: 2024-03-04 16:14:29
* @LastEditors: zhong
* @LastEditTime: 2024-03-04 20:06:54
* @FilePath: \big-event-vue\big-event-admin\src\views\article\ArticleCategory.vue
-->
<script setup>
import {
Edit,
Delete
} from '@element-plus/icons-vue'
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { getMySelfAllCategoryService, addCategoryService } from "@/api/article.js"
const categorys = ref([])
// 获取数据
const articleCategoryList = async () => {
let data = await getMySelfAllCategoryService();
categorys.value = data.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' },
]
}
// 添加文章分类
const addCategory = async () => {
// 调用接口
let result = await addCategoryService(categoryModel.value)
ElMessage.success(result.message ? result.message : "添加成功")
// 重新加载分类数据
articleCategoryList();
// 关闭弹窗
dialogVisible.value = false;
}
</script>
<template>
<el-card class="page-container">
<template #header>
<div class="header">
<span>文章分类</span>
<div class="extra">
<el-button type="primary" @click="dialogVisible = true">添加分类</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>
<!-- 添加分类弹窗 -->
<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" @click="addCategory()"> 确认 </el-button>
</span>
</template>
</el-dialog>
</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
<template>
文章管理
</template>
login
Login.vue
<script setup>
import { User, Lock } from "@element-plus/icons-vue";
import { ElMessage } from 'element-plus'
import { ref, reactive } from "vue";
import { useTokenStore } from '@/store/token.js'
import { userRegisterService, userLoginService } from "@/api/user.js";
// 导入并创建路由
import { useRouter } from "vue-router";
const router = useRouter();
//控制注册与登录表单的显示, 默认显示注册
const isRegister = ref(false);
// 清空数据模型
const clearDataAll = () => {
// 设置延时 清空太快了不习惯
setTimeout(() => {
registerData.value = {
username: "",
password: "",
rePassword: ""
}
}, 1000)
}
const registerData = ref({
username: "",
password: "",
rePassword: ""
})
// 密码重复校验规则
const checkRePassword = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次确认密码'))
} else if (value !== registerData.value.password) {
callback(new Error('两次密码不一致'))
} else {
callback()
}
}
// 表单校验规则
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 4, max: 12, message: '请输入 4-12 长度的用户名', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 12, message: '请输入 6-12 长度的密码', trigger: 'blur' },
],
rePassword: [
{ validator: checkRePassword, trigger: 'blur' },
{ min: 6, max: 12, message: '请输入 6-12 长度的密码', trigger: 'blur' },
]
}
// token 获取 token
const tokenStore = useTokenStore();
// 登录
const login = async () => {
// 调用登录请求
let result = await userLoginService(registerData.value);
ElMessage.success(result.message ? result.message : "登录成功");
// 登录成功存储 token
tokenStore.setToken(result.data);
// 跳转到首页
router.push("/");
}
// 注册
const register = async () => {
// 调用注册请求
let result = await userRegisterService(registerData.value)
ElMessage.success(result.message ? result.message : "注册成功");
}
</script>
<template>
<el-row class="login-page">
<el-col :span="12" class="bg"></el-col>
<el-col :span="6" :offset="3" class="form">
<div class="name">
<h1>欢迎访问后台</h1><h1>管理系统</h1>
</div>
<!-- 注册表单 -->
<el-form ref="form" size="large" autocomplete="off" v-if="isRegister" :model="registerData" :rules="rules">
<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 class="button" type="primary" auto-insert-space @click="register(); clearDataAll()">
注册
</el-button>
</el-form-item>
<el-form-item class="flex">
<el-link type="info" :underline="false" @click="isRegister = false">
← 返回
</el-link>
</el-form-item>
</el-form>
<!-- 登录表单 -->
<el-form ref="form" size="large" autocomplete="off" :model="registerData" :rules="rules" v-else>
<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(); clearDataAll()">登录</el-button>
</el-form-item>
<el-form-item class="flex">
<el-link type="info" :underline="false" @click="isRegister = true; clearDataAll()">
注册 →
</el-link>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<style lang="scss" scoped>
/* 样式 */
.login-page {
height: 100vh;
background-color: #fff;
background-color: #eaecf5;
.bg {
background: url("@/assets/back-jk.jpg") no-repeat center / cover;
border-radius: 0 20px 20px 0;
}
.form {
display: flex;
flex-direction: column;
justify-content: center;
user-select: none;
.name {
text-align: center;
font-size: 26px;
}
.title {
margin: 0 auto;
}
.button {
width: 100%;
}
.flex {
width: 100%;
display: flex;
justify-content: space-between;
}
}
}
</style>
main
Main.vue
<script setup>
import {
Management,
Promotion,
UserFilled,
User,
Crop,
EditPen,
SwitchButton,
CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'
</script>
<template>
<el-container class="layout-container">
<!-- 左侧菜单 -->
<el-aside width="300px">
<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 class="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/password">
<el-icon>
<EditPen />
</el-icon>
<span>重置密码</span>
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>
<!-- 右侧主区域 -->
<el-container>
<!-- 头部区域 -->
<el-header>
<div>云尚校园:<strong>小钟</strong></div>
<el-dropdown placement="bottom-end">
<span class="el-dropdown__box">
<el-avatar :src="avatar" />
<el-icon>
<CaretBottom />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile" :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>
</template>
</el-dropdown>
</el-header>
<!-- 中间区域 -->
<el-main>
<div style="height: 570px;border: 1px solid red;">
<router-view></router-view>
</div>
</el-main>
<!-- 底部区域 -->
<el-footer>云尚校园后台管理系统 ©2024 Created by 小钟</el-footer>
</el-container>
</el-container>
</template>
<style lang="scss" scoped>
span {
font-size: 20px;
}
.el-icon svg {
height: 3em;
width: 3em;
}
.layout-container {
height: 100vh;
.el-aside {
background-color: #232323;
margin-top: 20px;
&__logo {
height: 120px;
margin-top: 20px;
background: url('@/assets/back-jk.jpg') no-repeat center / 120px auto;
}
.el-menu {
justify-content: center;
border-right: none;
.el-menu-item {
align-items: center;
justify-content: center;
}
.sub_menu {
margin-left: 25%;
}
}
}
.el-header {
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 20px;
.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>
user
UserAvatar.vue
<template>
更换头像
</template>
UserInfo.vue
<template>
基本资料
</template>
UserResetPassword.vue
<template>
重置密码
</template>
App.vue
<script setup>
</script>
<template>
<router-view></router-view>
</template>
<style scoped></style>