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>
                


















