前端之vue3创建基本工程,基本登录、注册等功能的完整过程

发布于:2025-05-22 ⋅ 阅读:(15) ⋅ 点赞:(0)

此文也是为了做一个基本学习用的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、最终的项目结构:
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到