詳解Vite+JavaScript+Vue+Yarn實(shí)現(xiàn)登錄頁面實(shí)戰(zhàn)
本文將實(shí)現(xiàn)一個(gè)完整的登錄系統(tǒng)前端,包含登錄、注冊、登出和用戶管理功能。后端接口使用Mock.js模擬,不包含實(shí)際后端代碼。
項(xiàng)目結(jié)構(gòu)
login-demo/ ├── public/ ├── src/ │ ├── api/ # 接口定義 │ │ └── auth.js │ ├── components/ # 組件 │ │ ├── LoginForm.vue │ │ ├── RegisterForm.vue │ │ └── UserTable.vue │ ├── router/ # 路由 │ │ └── index.js │ ├── store/ # 狀態(tài)管理 │ │ └── auth.js │ ├── views/ # 頁面視圖 │ │ ├── Dashboard.vue │ │ ├── Login.vue │ │ └── Register.vue │ ├── App.vue │ ├── main.js │ └── mock.js # Mock數(shù)據(jù) ├── .gitignore ├── index.html ├── package.json ├── vite.config.js └── yarn.lock
完整代碼實(shí)現(xiàn)
1. 安裝依賴 (終端執(zhí)行)
yarn add vue-router@4 pinia axios mockjs
2. src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './mock' // 引入Mock數(shù)據(jù)
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')3. src/mock.js (模擬后端接口)
import Mock from 'mockjs'
// 模擬用戶數(shù)據(jù)
const users = [
{ id: 1, username: 'admin', password: 'admin123', email: 'admin@example.com', role: 'admin' },
{ id: 2, username: 'user1', password: 'user123', email: 'user1@example.com', role: 'user' }
]
// 登錄接口
Mock.mock('/api/login', 'post', (options) => {
const { username, password } = JSON.parse(options.body)
const user = users.find(u => u.username === username && u.password === password)
if (user) {
return Mock.mock({
code: 200,
message: '登錄成功',
data: {
token: '@guid',
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role
}
}
})
}
return Mock.mock({
code: 401,
message: '用戶名或密碼錯(cuò)誤'
})
})
// 登出接口
Mock.mock('/api/logout', 'post', {
code: 200,
message: '登出成功'
})
// 注冊接口
Mock.mock('/api/register', 'post', (options) => {
const data = JSON.parse(options.body)
const existingUser = users.find(u => u.username === data.username || u.email === data.email)
if (existingUser) {
return Mock.mock({
code: 400,
message: '用戶名或郵箱已存在'
})
}
const newUser = {
id: users.length + 1,
username: data.username,
password: data.password,
email: data.email,
role: 'user'
}
users.push(newUser)
return Mock.mock({
code: 200,
message: '注冊成功',
data: {
user: {
id: newUser.id,
username: newUser.username,
email: newUser.email,
role: newUser.role
}
}
})
})
// 獲取用戶列表
Mock.mock('/api/users', 'get', () => {
return Mock.mock({
code: 200,
message: '獲取用戶列表成功',
data: {
users: users.map(u => ({
id: u.id,
username: u.username,
email: u.email,
role: u.role
}))
}
})
})
// 刪除用戶
Mock.mock(/\/api\/users\/\d+/, 'delete', (options) => {
const id = options.url.split('/').pop()
const index = users.findIndex(u => u.id == id)
if (index !== -1) {
users.splice(index, 1)
return Mock.mock({
code: 200,
message: '用戶刪除成功'
})
}
return Mock.mock({
code: 404,
message: '用戶不存在'
})
})4. src/api/auth.js (接口封裝)
import axios from 'axios'
const api = axios.create({
baseURL: '/api',
timeout: 5000
})
export default {
// 登錄
login(credentials) {
return api.post('/login', credentials)
},
// 登出
logout() {
return api.post('/logout')
},
// 注冊
register(userData) {
return api.post('/register', userData)
},
// 獲取用戶列表
getUsers() {
return api.get('/users')
},
// 刪除用戶
deleteUser(id) {
return api.delete(`/users/${id}`)
}
}5. src/store/auth.js (Pinia狀態(tài)管理)
import { defineStore } from 'pinia'
import authApi from '@/api/auth'
export const useAuthStore = defineStore('auth', {
state: () => ({
user: JSON.parse(localStorage.getItem('user')) || null,
token: localStorage.getItem('token') || null,
isAuthenticated: !!localStorage.getItem('token'),
users: []
}),
actions: {
async login(credentials) {
try {
const response = await authApi.login(credentials)
if (response.data.code === 200) {
this.user = response.data.data.user
this.token = response.data.data.token
this.isAuthenticated = true
// 保存到本地存儲(chǔ)
localStorage.setItem('user', JSON.stringify(this.user))
localStorage.setItem('token', this.token)
return true
}
return false
} catch (error) {
console.error('登錄失敗:', error)
return false
}
},
async logout() {
try {
await authApi.logout()
this.user = null
this.token = null
this.isAuthenticated = false
// 清除本地存儲(chǔ)
localStorage.removeItem('user')
localStorage.removeItem('token')
return true
} catch (error) {
console.error('登出失敗:', error)
return false
}
},
async register(userData) {
try {
const response = await authApi.register(userData)
return response.data.code === 200
} catch (error) {
console.error('注冊失敗:', error)
return false
}
},
async fetchUsers() {
try {
const response = await authApi.getUsers()
if (response.data.code === 200) {
this.users = response.data.data.users
return true
}
return false
} catch (error) {
console.error('獲取用戶列表失敗:', error)
return false
}
},
async deleteUser(id) {
try {
const response = await authApi.deleteUser(id)
if (response.data.code === 200) {
// 重新獲取用戶列表
await this.fetchUsers()
return true
}
return false
} catch (error) {
console.error('刪除用戶失敗:', error)
return false
}
}
}
})6. src/router/index.js (路由配置)
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/store/auth'
const routes = [
{
path: '/',
redirect: '/login'
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { requiresGuest: true }
},
{
path: '/register',
name: 'Register',
component: () => import('@/views/Register.vue'),
meta: { requiresGuest: true }
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { requiresAuth: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
// 檢查路由是否需要認(rèn)證
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
return next({ name: 'Login' })
}
// 檢查路由是否需要未登錄狀態(tài)
if (to.meta.requiresGuest && authStore.isAuthenticated) {
return next({ name: 'Dashboard' })
}
next()
})
export default router7. src/views/Login.vue
<template>
<div class="login-container">
<div class="login-card">
<h1>用戶登錄</h1>
<LoginForm @login="handleLogin" />
<div class="links">
<router-link to="/register">注冊新賬號</router-link>
</div>
</div>
</div>
</template>
<script>
import LoginForm from '@/components/LoginForm.vue'
import { useAuthStore } from '@/store/auth'
import { useRouter } from 'vue-router'
export default {
components: {
LoginForm
},
setup() {
const authStore = useAuthStore()
const router = useRouter()
const handleLogin = async (credentials) => {
const success = await authStore.login(credentials)
if (success) {
router.push({ name: 'Dashboard' })
} else {
alert('登錄失敗,請檢查用戶名和密碼')
}
}
return {
handleLogin
}
}
}
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
}
.login-card {
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
padding: 30px;
width: 100%;
max-width: 400px;
text-align: center;
}
h1 {
margin-bottom: 24px;
color: #333;
font-size: 24px;
}
.links {
margin-top: 20px;
font-size: 14px;
}
.links a {
color: #2575fc;
text-decoration: none;
}
.links a:hover {
text-decoration: underline;
}
</style>8. src/components/LoginForm.vue
<template>
<form @submit.prevent="submitForm">
<div class="form-group">
<label for="username">用戶名</label>
<input
type="text"
id="username"
v-model="form.username"
placeholder="請輸入用戶名"
required
/>
</div>
<div class="form-group">
<label for="password">密碼</label>
<input
type="password"
id="password"
v-model="form.password"
placeholder="請輸入密碼"
required
/>
</div>
<button type="submit" class="login-btn">登錄</button>
</form>
</template>
<script>
import { ref } from 'vue'
export default {
emits: ['login'],
setup(props, { emit }) {
const form = ref({
username: '',
password: ''
})
const submitForm = () => {
emit('login', {
username: form.value.username,
password: form.value.password
})
}
return {
form,
submitForm
}
}
}
</script>
<style scoped>
.form-group {
margin-bottom: 20px;
text-align: left;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #555;
}
input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus {
border-color: #2575fc;
outline: none;
}
.login-btn {
width: 100%;
padding: 12px;
background: #2575fc;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.3s;
}
.login-btn:hover {
background: #1c65e0;
}
</style>9. src/views/Register.vue
<template>
<div class="register-container">
<div class="register-card">
<h1>用戶注冊</h1>
<RegisterForm @register="handleRegister" />
<div class="links">
<router-link to="/login">已有賬號?立即登錄</router-link>
</div>
</div>
</div>
</template>
<script>
import RegisterForm from '@/components/RegisterForm.vue'
import { useAuthStore } from '@/store/auth'
import { useRouter } from 'vue-router'
export default {
components: {
RegisterForm
},
setup() {
const authStore = useAuthStore()
const router = useRouter()
const handleRegister = async (userData) => {
const success = await authStore.register(userData)
if (success) {
alert('注冊成功!請登錄')
router.push({ name: 'Login' })
} else {
alert('注冊失敗,用戶名或郵箱可能已被使用')
}
}
return {
handleRegister
}
}
}
</script>
<style scoped>
.register-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, #ff6b6b 0%, #ffa502 100%);
}
.register-card {
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
padding: 30px;
width: 100%;
max-width: 400px;
text-align: center;
}
h1 {
margin-bottom: 24px;
color: #333;
font-size: 24px;
}
.links {
margin-top: 20px;
font-size: 14px;
}
.links a {
color: #ff6b6b;
text-decoration: none;
}
.links a:hover {
text-decoration: underline;
}
</style>10. src/components/RegisterForm.vue
<template>
<form @submit.prevent="submitForm">
<div class="form-group">
<label for="username">用戶名</label>
<input
type="text"
id="username"
v-model="form.username"
placeholder="請輸入用戶名"
required
/>
</div>
<div class="form-group">
<label for="email">郵箱</label>
<input
type="email"
id="email"
v-model="form.email"
placeholder="請輸入郵箱"
required
/>
</div>
<div class="form-group">
<label for="password">密碼</label>
<input
type="password"
id="password"
v-model="form.password"
placeholder="請輸入密碼"
required
/>
</div>
<div class="form-group">
<label for="confirmPassword">確認(rèn)密碼</label>
<input
type="password"
id="confirmPassword"
v-model="form.confirmPassword"
placeholder="請?jiān)俅屋斎朊艽a"
required
/>
</div>
<button type="submit" class="register-btn">注冊</button>
</form>
</template>
<script>
import { ref } from 'vue'
export default {
emits: ['register'],
setup(props, { emit }) {
const form = ref({
username: '',
email: '',
password: '',
confirmPassword: ''
})
const submitForm = () => {
if (form.value.password !== form.value.confirmPassword) {
alert('兩次輸入的密碼不一致')
return
}
emit('register', {
username: form.value.username,
email: form.value.email,
password: form.value.password
})
}
return {
form,
submitForm
}
}
}
</script>
<style scoped>
.form-group {
margin-bottom: 20px;
text-align: left;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #555;
}
input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus {
border-color: #ff6b6b;
outline: none;
}
.register-btn {
width: 100%;
padding: 12px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.3s;
}
.register-btn:hover {
background: #ff5252;
}
</style>11. src/views/Dashboard.vue
<template>
<div class="dashboard-container">
<header class="header">
<div class="user-info">
<span>歡迎, {{ user.username }} ({{ user.role }})</span>
</div>
<button class="logout-btn" @click="handleLogout">登出</button>
</header>
<main class="main-content">
<h1>用戶管理系統(tǒng)</h1>
<div class="actions">
<button class="refresh-btn" @click="fetchUsers">刷新用戶列表</button>
</div>
<UserTable :users="users" @delete-user="deleteUser" />
</main>
</div>
</template>
<script>
import UserTable from '@/components/UserTable.vue'
import { useAuthStore } from '@/store/auth'
import { useRouter } from 'vue-router'
import { computed, onMounted, ref } from 'vue'
export default {
components: {
UserTable
},
setup() {
const authStore = useAuthStore()
const router = useRouter()
const users = ref([])
const user = computed(() => authStore.user)
const fetchUsers = async () => {
await authStore.fetchUsers()
users.value = authStore.users
}
const deleteUser = async (id) => {
const confirmDelete = confirm('確定要?jiǎng)h除這個(gè)用戶嗎?')
if (confirmDelete) {
const success = await authStore.deleteUser(id)
if (success) {
await fetchUsers()
}
}
}
const handleLogout = async () => {
await authStore.logout()
router.push({ name: 'Login' })
}
onMounted(() => {
fetchUsers()
})
return {
user,
users,
fetchUsers,
deleteUser,
handleLogout
}
}
}
</script>
<style scoped>
.dashboard-container {
min-height: 100vh;
background-color: #f5f7fa;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 40px;
background-color: white;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.user-info {
font-weight: 500;
color: #333;
}
.logout-btn {
padding: 8px 16px;
background-color: #ff4757;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.logout-btn:hover {
background-color: #ff2e43;
}
.main-content {
max-width: 1200px;
margin: 30px auto;
padding: 0 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
color: #2c3e50;
}
.actions {
margin-bottom: 20px;
display: flex;
justify-content: flex-end;
}
.refresh-btn {
padding: 8px 16px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.refresh-btn:hover {
background-color: #2980b9;
}
</style>12. src/components/UserTable.vue
<template>
<div class="user-table">
<table>
<thead>
<tr>
<th>ID</th>
<th>用戶名</th>
<th>郵箱</th>
<th>角色</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{{ user.role }}</td>
<td>
<button
class="delete-btn"
@click="$emit('delete-user', user.id)"
:disabled="user.id === currentUserId"
>
刪除
</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import { computed } from 'vue'
import { useAuthStore } from '@/store/auth'
export default {
props: {
users: {
type: Array,
required: true
}
},
emits: ['delete-user'],
setup() {
const authStore = useAuthStore()
const currentUserId = computed(() => authStore.user?.id || null)
return {
currentUserId
}
}
}
</script>
<style scoped>
.user-table {
overflow-x: auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 16px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background-color: #f8f9fa;
font-weight: 600;
color: #495057;
}
tbody tr:hover {
background-color: #f8f9fa;
}
.delete-btn {
padding: 6px 12px;
background-color: #ff6b6b;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.delete-btn:hover:not(:disabled) {
background-color: #ff5252;
}
.delete-btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
</style>13. src/App.vue
<template>
<router-view />
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f8f9fa;
}
#app {
min-height: 100vh;
}
</style>項(xiàng)目啟動(dòng)步驟
1)創(chuàng)建項(xiàng)目
yarn create vite login-demo --template vue cd login-demo
2)安裝依賴
yarn add vue-router@4 pinia axios mockjs
3)創(chuàng)建項(xiàng)目結(jié)構(gòu)
創(chuàng)建上述所有文件和目錄
4)啟動(dòng)開發(fā)服務(wù)器
yarn dev
5)訪問應(yīng)用
打開瀏覽器訪問 http://localhost:5173
功能說明
1)登錄功能
- 使用用戶名和密碼登錄
- 模擬后端驗(yàn)證
- 登錄成功后跳轉(zhuǎn)到儀表盤
2)注冊功能
- 創(chuàng)建新用戶賬號
- 驗(yàn)證用戶名和郵箱唯一性
- 注冊成功后跳轉(zhuǎn)到登錄頁面
3)登出功能
- 清除用戶會(huì)話
- 重定向到登錄頁面
4)用戶管理
- 查看所有用戶列表
- 刪除用戶(不能刪除當(dāng)前登錄用戶)
- 刷新用戶列表
接口定義
POST /api/login- 用戶登錄POST /api/logout- 用戶登出POST /api/register- 用戶注冊GET /api/users- 獲取用戶列表DELETE /api/users/:id- 刪除用戶
這個(gè)登錄系統(tǒng)前端包含了完整的用戶認(rèn)證流程和簡單的用戶管理功能,使用了Vite作為構(gòu)建工具,Vue3作為前端框架,Pinia進(jìn)行狀態(tài)管理,Vue Router處理路由,Mock.js模擬后端接口。
到此這篇關(guān)于詳解Vite+JavaScript+Vue+Yarn實(shí)現(xiàn)登錄頁面實(shí)戰(zhàn)的文章就介紹到這了,更多相關(guān)Vue Yarn登錄頁內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue實(shí)現(xiàn)省市區(qū)級聯(lián)下拉選擇框
這篇文章主要為大家詳細(xì)介紹了Vue實(shí)現(xiàn)省市區(qū)級聯(lián)下拉選擇框,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
vue 頁面回退mounted函數(shù)不執(zhí)行的解決方案
這篇文章主要介紹了vue 頁面回退mounted函數(shù)不執(zhí)行的解決方案 ,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-07-07
Vue前端如何設(shè)置Cookie和鑒權(quán)問題詳解
這篇文章主要介紹了前端如何設(shè)置和使用Cookie,并對比了Cookie和Token在鑒權(quán)中的優(yōu)缺點(diǎn),文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-02-02
vue出現(xiàn)Uncaught SyntaxError:Unexpected token問題及解決
這篇文章主要介紹了vue出現(xiàn)Uncaught SyntaxError:Unexpected token問題及解決,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-04-04
結(jié)合康熙選秀講解vue虛擬列表實(shí)現(xiàn)
這篇文章主要為大家介紹了結(jié)合康熙選秀講解vue虛擬列表的原理使用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
vue項(xiàng)目webpack中配置src路徑別名及使用方式
這篇文章主要介紹了vue項(xiàng)目webpack中配置src路徑別名及使用方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03
vue3開啟攝像頭并進(jìn)行拍照的實(shí)現(xiàn)示例
本文主要介紹了vue3開啟攝像頭并進(jìn)行拍照的實(shí)現(xiàn)示例,主要是使用navigator.mediaDevices.getUserMedia這個(gè)API來實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01
vue elementUI el-form 數(shù)據(jù)無法賦值且不報(bào)錯(cuò)的問題及解決方法
vue項(xiàng)目中使用elementUI的el-form組件,里面有部分后端數(shù)據(jù)遍歷的字段和部分確定的字段,遇到個(gè)問題遍歷的字段可以修改值但是確定的幾個(gè)字段無法修改值,下面小編給大家分享vue elementUI el-form 數(shù)據(jù)無法賦值且不報(bào)錯(cuò)的問題及解決方法,感興趣的朋友一起看看吧2023-12-12

