此文也是为了做一个基本学习用的vue3创建项目的过程,包含基本的登录页面、登出页面、基本的router跳转、axios调用、登录验证等内容。与项目:
https://gitee.com/rainpet/java-web-demo/tree/master/spring-security01
可以配套使用。
如下为主要过程。
1、node.js下载地址:
https://mirrors.tuna.tsinghua.edu.cn/nodejs-release/
下载一个LTS版本的 v20和v22都是
https://mirrors.tuna.tsinghua.edu.cn/nodejs-release/v20.19.2/
https://mirrors.tuna.tsinghua.edu.cn/nodejs-release/v22.15.1/
这里下载v20的。
2、解压到路径,我这里用的是:
D:\wamp\node-v20
设置系统path变量
设置好后,可以通过
node --version
来查看版本是否生效。
3、全局安装基本的包:工程工具(vite)、包管理工具(yarn、cnpm)
npm install -g yarn
npm install -g cnpm
npm install -g vite
4、项目创建,咱们的目标是要创建一个spa(single page application)程序,这也是vue的主流应用方式。
切换到工程目录:
d:
cd D:\java\workspace>
创建工程命令
npm create vite@latest
输入工程名称:vuedemo01
选择框架:Vue
选择类型:JavaScript
完成创建。
5、安装vscode,安装相应的扩展:
Vue - Official
6、进入到vscode,打开终端,使用cmd模式。
安装本项目的依赖
yarn add axios
yarn add element-plus
yarn add vue-router
yarn add @element-plus/icons-vue
yarn add vite-plugin-vue-devtools
7、打开项目,修改vie.config.js文件,增加后台api的代理设置,确认有如下的server的相关内容。
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
server: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:8086',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),//去掉原始api的前缀
},
},
},
})
8、src下,创建目录:router,目录中增加文件:index.js
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
//component: Home,
component: () => import('../views/HomeView.vue'),
}
],
})
export default router
9、修改入口的js文件,src/main.js,将组建加入到Vue的上下文:
由:
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
改为:
import { createApp } from 'vue'
//import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
//app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app')
10、创建组件-Header.vue:
<template>
<el-header class="header">
<div class="logo">
<h2>Vue Demo</h2>
</div>
<div class="header-right">
<el-dropdown>
<span class="el-dropdown-link">
Admin
<el-icon class="el-icon--right"><arrow-down /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleCommand('info')">关于</el-dropdown-item>
<el-dropdown-item @click="handleCommand('logout')">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
</template>
<script setup>
import { ArrowDown } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const handleCommand = (command) => {
switch (command) {
case 'info':
router.push('/about')
break
case 'logout':
// 这里可以添加退出登录的逻辑
console.log('退出登录')
router.push('/logout')
break
}
}
</script>
<style scoped>
.header {
background-color: #409EFF;
color: white;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
}
.logo h2 {
margin: 0;
}
.header-right {
display: flex;
align-items: center;
}
.el-dropdown-link {
color: white;
cursor: pointer;
display: flex;
align-items: center;
}
</style>
11、组件 Footer.vue
<template>
<el-footer class="footer">
<p>© 2024 Vue Demo. All rights reserved.</p>
</el-footer>
</template>
<script setup>
</script>
<style scoped>
.footer {
background-color: #f5f7fa;
color: #606266;
text-align: center;
padding: 20px 0;
border-top: 1px solid #e4e7ed;
}
.footer p {
margin: 0;
}
</style>
12、组件:SideMenu.vue
<template>
<el-menu
class="side-menu el-menu-vertical"
:default-active="activeIndex"
:collapse="isCollapse"
router
>
<el-menu-item index="/">
<el-icon><HomeFilled /></el-icon>
<template #title>首页</template>
</el-menu-item>
<el-sub-menu index="/about">
<template #title>
<el-icon><Document /></el-icon>
<span>功能</span>
</template>
<el-menu-item index="/order/list">订单</el-menu-item>
</el-sub-menu>
</el-menu>
</template>
<script setup>
import { ref } from 'vue'
import { HomeFilled, Document, Setting, InfoFilled } from '@element-plus/icons-vue'
const activeIndex = ref('/')
const isCollapse = ref(false)
</script>
<style scoped>
.side-menu {
height: 100%;
border-right: none;
}
.el-menu-vertical:not(.el-menu--collapse) {
width: 200px;
}
</style>
13、IndexView.vue
<template>
<div class="dashboard-container">
<el-row :gutter="20">
<el-col :span="6">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>访问量</span>
</div>
</template>
<div class="card-content">
<h2>1,234</h2>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>用户数</span>
</div>
</template>
<div class="card-content">
<h2>567</h2>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>订单数</span>
</div>
</template>
<div class="card-content">
<h2>890</h2>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>收入</span>
</div>
</template>
<div class="card-content">
<h2>¥12,345</h2>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
</script>
<style scoped>
.dashboard-container {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-content {
text-align: center;
}
.card-content h2 {
margin: 10px 0;
color: #409EFF;
}
</style>
14、HomeView.vue
<template>
<el-container class="layout-container">
<Header />
<el-container class="main-container">
<el-aside width="200px" class="aside">
<SideMenu />
</el-aside>
<el-container class="content-container">
<el-main>
<router-view>
<IndexView />
</router-view>
</el-main>
<Footer />
</el-container>
</el-container>
</el-container>
</template>
<script setup>
import Header from '@/components/Header.vue'
import Footer from '@/components/Footer.vue'
import SideMenu from '@/components/SideMenu.vue'
import IndexView from '@/views/IndexView.vue'
</script>
<style scoped>
.layout-container {
height: 100vh;
display: flex;
flex-direction: column;
}
.main-container {
flex: 1;
}
.aside {
background-color: #fff;
border-right: 1px solid #e6e6e6;
}
.content-container {
flex: 1;
display: flex;
flex-direction: column;
}
.el-main {
flex: 1;
background-color: #f5f7fa;
padding: 20px;
}
</style>
15、AboutView.vue
<template>
<el-container class="layout-container">
<Header />
<el-container class="main-container">
<el-aside width="200px" class="aside">
<SideMenu />
</el-aside>
<el-container class="content-container">
<el-main>
<router-view>
<div class="about-content">
<el-descriptions :column="1" border>
<el-descriptions-item label="系统名称">Vue Demo System</el-descriptions-item>
<el-descriptions-item label="版本">1.0.0</el-descriptions-item>
<el-descriptions-item label="技术栈">
Vue 3 + Element Plus + Vue Router
</el-descriptions-item>
<el-descriptions-item label="开发团队">Demo Team</el-descriptions-item>
</el-descriptions>
</div>
</router-view>
</el-main>
<Footer />
</el-container>
</el-container>
</el-container>
</template>
<script setup>
import Header from '@/components/Header.vue'
import Footer from '@/components/Footer.vue'
import SideMenu from '@/components/SideMenu.vue'
</script>
<style scoped>
.about-container {
padding: 20px;
}
.about-card {
max-width: 800px;
margin: 0 auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-header h2 {
margin: 0;
color: #409EFF;
}
.about-content {
margin-top: 20px;
}
.layout-container {
height: 100vh;
display: flex;
flex-direction: column;
}
.main-container {
flex: 1;
}
.aside {
background-color: #fff;
border-right: 1px solid #e6e6e6;
}
.content-container {
flex: 1;
display: flex;
flex-direction: column;
}
.el-main {
flex: 1;
background-color: #f5f7fa;
padding: 20px;
}
</style>
16、App.vue 最终修改为:
<script setup>
</script>
<template>
<router-view></router-view>
</template>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
#app {
height: 100%;
width: 100%;
}
</style>
17、登录LoginView.vue
<template>
<div class="login-container">
<div class="login-box">
<h1>登录</h1>
<el-form @submit.prevent="handleLogin" label-position="top" class="login-form">
<el-form-item label="用户名">
<el-input v-model="username" required @keyup.enter="handleLogin"></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input type="password" v-model="password" required @keyup.enter="handleLogin"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin" class="login-button">登录</el-button>
</el-form-item>
</el-form>
<p v-if="errorMessage" class="error">{{ errorMessage }}</p>
<p class="register-link">还没有账号?<a href="/register">注册</a></p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import axios from 'axios';
import { useRouter } from 'vue-router';
import { ElForm, ElFormItem, ElInput, ElButton, ElMessage } from 'element-plus';
const username = ref('');
const password = ref('');
const errorMessage = ref('');
const router = useRouter();
const handleLogin = async () => {
try {
const response = await axios.post('/api/user/login', {
userName: username.value,
password: password.value
});
if (response.data.code === 200) {
errorMessage.value = '';
localStorage.setItem('token', response.data.data.token);
localStorage.setItem('userId', response.data.data.userId);
localStorage.setItem('userName', response.data.data.userName);
router.push('/');
} else {
errorMessage.value = response.data.msg;
}
} catch (error) {
errorMessage.value = '登录失败,请检查用户名和密码';
}
};
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
/* background: linear-gradient(135deg, #6e8efb, #a777e3); */
}
.login-box {
width: 400px;
padding: 40px;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
text-align: center;
}
.login-box h1 {
margin-bottom: 30px;
font-size: 24px;
color: #333;
}
.login-form {
display: flex;
flex-direction: column;
}
.login-form .el-form-item {
margin-bottom: 20px;
}
.login-button {
width: 100%;
background-color: #6e8efb;
border-color: #6e8efb;
}
.login-button:hover {
background-color: #5a7de1;
border-color: #5a7de1;
}
.error {
color: red;
margin-top: 10px;
}
.register-link {
margin-top: 20px;
color: #333;
}
.register-link a {
color: #6e8efb;
text-decoration: none;
}
.register-link a:hover {
text-decoration: underline;
}
</style>
18、登出页面 LogoutView.vue
<template>
<div class="logout-container">
<Header />
<el-container class="content">
<el-main>
<h1>您已退出登录!</h1>
<el-button type="primary" @click="goHome">返回首页</el-button>
</el-main>
</el-container>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
import { useRouter } from 'vue-router';
import Header from '@/components/Header.vue';
const router = useRouter();
const goHome = () => {
router.push('/');
};
onMounted(() => {
// 退出登录逻辑,删除存储的 token
localStorage.removeItem('token');
});
</script>
<style scoped>
.logout-container {
text-align: center;
padding: 2rem;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: calc(100vh - 200px); /* Adjust height based on header */
}
h1 {
margin-bottom: 1rem;
}
.el-button {
margin-top: 2rem;
}
</style>
19、注册页面 RegisterView.vue
<template>
<div class="register-container">
<h1>注册</h1>
<el-form :model="form" @submit.prevent="submitForm" ref="registerForm" label-position="top" class="register-form">
<el-form-item label="用户名" :label-width="formLabelWidth">
<el-input v-model="form.userName" required></el-input>
</el-form-item>
<el-form-item label="密码" :label-width="formLabelWidth">
<el-input type="password" v-model="form.password" required></el-input>
</el-form-item>
<el-form-item label="确认密码" :label-width="formLabelWidth">
<el-input type="password" v-model="confirmPassword" @input="checkPasswordMatch" required></el-input>
<span v-if="passwordMismatch" class="error">密码不一致</span>
</el-form-item>
<el-form-item label="邮箱" :label-width="formLabelWidth">
<el-input type="email" v-model="form.email" required></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm" class="register-button">注册</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { ElForm, ElFormItem, ElInput, ElButton, ElMessage } from 'element-plus';
import axios from 'axios';
const form = ref({
userName: '',
password: '',
email: ''
});
const confirmPassword = ref('');
const passwordMismatch = ref(false);
const formLabelWidth = '100px';
const router = useRouter();
const checkPasswordMatch = () => {
passwordMismatch.value = form.value.password !== confirmPassword.value;
};
const submitForm = async () => {
checkPasswordMatch();
if (!passwordMismatch.value) {
try {
const response = await axios.post('/api/user/register', form.value);
if (response.data.code === 200) {
ElMessage.success('注册成功');
router.push('/login');
} else {
ElMessage.error('注册失败,请重试,' + response.data.msg);
}
} catch (error) {
ElMessage.error('注册失败,请重试');
}
}
};
</script>
<style scoped>
.register-container {
max-width: 500px;
margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.register-container h1 {
text-align: center;
margin-bottom: 20px;
}
.register-form {
display: flex;
flex-direction: column;
}
.register-button {
width: 100%;
}
.error {
color: red;
font-size: 12px;
margin-top: 5px;
}
</style>
20、修改src/router/index.js,增加如下内容,以实现登录验证:
router.beforeEach((to, from, next) => {
if (to.name !== 'login' && to.name !== 'logout' && to.name !=='register' && !localStorage.getItem('token')) {
next({ name: 'login' })
} else {
next()
}
})
21、增加最终的登录后测试页面 OrderView.vue:
<template>
<div>
<Header />
<div class="layout">
<Sidebar />
<div class="content">
<section class="order">
<h4>订单列表</h4>
<div v-if="orders.length > 0">
<el-table :data="orders" style="width: 100%">
<el-table-column prop="fdate" label="订单日期" />
<el-table-column prop="orderAmount" label="订单金额" />
<el-table-column prop="status" label="订单状态" />
<el-table-column prop="address" label="收货地址" />
<el-table-column label="操作">
<template #default="scope">
<el-button type="primary" @click="placeOrder(scope.row)">支付</el-button>
<el-button type="info" @click="showDetails(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<div class="order-total">
<h3>总计: {{ formatCurrency(orderAmounts) }}</h3>
</div>
</div>
<div v-else>
<p>订单是空的。</p>
</div>
</section>
</div>
</div>
<Footer />
<el-dialog
v-model="dialogVisible"
title="Tips"
width="800"
:before-close="handleClose"
>
<template #title>
<span>订单详情</span>
</template>
<el-table :data="selectedOrder.entries" style="width: 100%">
<el-table-column prop="goodsName" label="商品ID" />
<el-table-column prop="qty" label="数量" />
<el-table-column prop="price" label="价格">
<template #default="entryScope">
<span>¥{{ entryScope.row.price }}</span>
</template>
</el-table-column>
<el-table-column prop="orderAmount" label="金额">
<template #default="entryScope">
<span>¥{{ entryScope.row.amount }}</span>
</template>
</el-table-column>
</el-table>
<div class="order-total">
<h3>订单小计: {{ formatCurrency(detailAmount) }}</h3>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">关闭</el-button>
</span>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import Header from '@/components/Header.vue';
import Sidebar from '@/components/Sidemenu.vue';
import Footer from '@/components/Footer.vue';
import axios from 'axios';
import { useRouter } from 'vue-router';
const router = useRouter();
const orders = ref([]);
const dialogVisible = ref(false);
const selectedOrder = ref({ entries: [] });
const getOrdersList = async () => {
try {
const token = localStorage.getItem('token');
const userId = localStorage.getItem('userId');
const headers = {
'token': `${token}`
};
const response = await axios.get('/api/orders/listByOwner/' + userId, { headers: headers });
//console.log(response.data);
console.log(response.data.data);
// 检查响应数据格式
if (Array.isArray(response.data.data)) {
orders.value = response.data.data;
} else if (response.data.code === 401) {
router.push('/login');
} else {
console.error('服务器返回的数据格式不正确:', response.data);
}
} catch (error) {
console.error('获取订单列表失败:', error);
}
};
const formatCurrency = (amount) => {
return amount.toLocaleString('zh-CN', { style: 'currency', currency: 'CNY' }).replace('CNY', '');
};
const detailAmount = computed(() => {
return Math.round(selectedOrder.value.entries.reduce((total, entry) => total + entry.amount, 0),2);
});
const orderAmounts = computed(() => {
return orders.value.reduce((total, order) => {
return Math.round(total + order.entries.reduce((orderTotal, entry) => orderTotal + entry.amount, 0),2);
}, 0);
});
const placeOrder = (order) => {
// 支付逻辑
alert(`订单 ${order.id} 已支付`);
};
const showDetails = (order) => {
console.log(`查看订单详情: ${order.id}`);
selectedOrder.value = order;
console.log(selectedOrder.value);
dialogVisible.value = true;
console.log(dialogVisible.value);
};
const handleClose = (done) => {
dialogVisible.value = false;
};
onMounted(() => {
getOrdersList();
});
</script>
<style scoped>
.layout {
display: flex;
}
.el-dialog {
display: block !important;
}
.content {
flex: 1;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
background-color: #fff;
overflow-y: auto;
}
.order {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 2rem;
}
.order-total {
text-align: right;
margin-top: 2rem;
}
.order-total h3 {
font-size: 1.5rem;
color: #333;
}
.el-button {
margin-top: 1rem;
}
</style>
22、增加相应的router的内容
{
path: '/logout',
name: 'logout',
component: () => import('../views/LogoutView.vue')
},
{
path:'/login',
name: 'login',
component: () => import('../views/LoginView.vue')
},
{
path: '/register',
name: 'register',
component: () => import('../views/RegisterView.vue')
},
{
path: '/order/list',
name: 'orderList',
component: () => import('../views/OrderView.vue')
}
23、最终的项目结构: