<think>我们使用Vue3和Element Plus来构建一个登录页面,并实现与Django DRF后端的交互。以下是步骤:
1. 安装Element Plus
2. 创建登录页面组件
3. 使用Element Plus的表单组件
4. 处理登录逻辑,包括发送请求、保存token
5. 设置axios拦截器(可选,用于自动携带token)
注意:我们将使用axios发送请求,所以需要安装axios。
步骤:
### 1. 安装Element Plus和axios
在项目目录下运行:
```bash
npm install element-plus axios
```
### 2. 在main.js中引入Element Plus
```javascript
import { createApp } from 'vue'
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(router)
app.use(ElementPlus)
app.mount('#app')
```
### 3. 创建登录页面组件(Login.vue)
```vue
<template>
<div class="login-container">
<el-card class="login-card">
<h2 class="login-title">系统登录</h2>
<el-form
ref="loginForm"
:model="loginForm"
:rules="loginRules"
@keyup.enter="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名"
prefix-icon="el-icon-user"
></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
placeholder="请输入密码"
prefix-icon="el-icon-lock"
show-password
></el-input>
</el-form-item>
<el-form-item>
<el-button
type="primary"
style="width:100%;"
:loading="loading"
@click="handleLogin"
>登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
import { ElMessage } from 'element-plus'
const router = useRouter()
// 表单数据
const loginForm = ref({
username: '',
password: ''
})
// 表单验证规则
const loginRules = ref({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' }
]
})
// 加载状态
const loading = ref(false)
const loginFormRef = ref(null)
// 登录方法
const handleLogin = () => {
loginFormRef.value.validate(valid => {
if (!valid) {
return false
}
loading.value = true
// 发送登录请求
axios.post('http://your-django-backend/api/login/', loginForm.value)
.then(response => {
// 登录成功
loading.value = false
// 保存token到localStorage
localStorage.setItem('authToken', response.data.token)
// 设置axios的全局header
axios.defaults.headers.common['Authorization'] = `Token ${response.data.token}`
// 跳转到首页
router.push('/')
})
.catch(error => {
loading.value = false
ElMessage.error('登录失败: ' + (error.response?.data?.detail || '未知错误'))
})
})
}
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f5f5f5;
}
.login-card {
width: 400px;
}
.login-title {
text-align: center;
margin-bottom: 20px;
}
</style>
```
### 4. 配置路由(router/index.js)
```javascript
import { createRouter, createWebHistory } from 'vue-router'
import Login from '../views/Login.vue'
const routes = [
{
path: '/login',
name: 'Login',
component: Login
},
// 其他路由...
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
```
### 5. 在App.vue中设置路由视图
```vue
<template>
<router-view />
</template>
```
### 6. 设置axios拦截器(可选,用于处理token过期等)
在main.js或者单独创建一个axios实例,并设置请求拦截器和响应拦截器。
例如,在请求拦截器中添加token:
```javascript
// 在main.js中
import axios from 'axios'
// 从localStorage中获取token
const token = localStorage.getItem('authToken')
if (token) {
axios.defaults.headers.common['Authorization'] = `Token ${token}`
}
// 响应拦截器,处理401未授权
axios.interceptors.response.use(
response => response,
error => {
if (error.response && error.response.status === 401) {
// 清除token并跳转到登录页
localStorage.removeItem('authToken')
delete axios.defaults.headers.common['Authorization']
// 使用路由跳转,注意这里需要获取router实例,可能需要将router导出
// 假设我们已经导出了router,或者可以在拦截器里使用router
// router.push('/login')
}
return Promise.reject(error)
}
)
```
注意:在响应拦截器中使用router,需要将router实例导出,并在拦截器中导入。或者可以在创建axios实例的地方处理。
### 7. Django DRF后端设置
请参考之前提供的Django DRF登录视图示例,确保后端接口正确返回token。
### 8. 跨域设置
在Django中安装`django-cors-headers`并配置,允许Vue前端域名的跨域请求。
### 相关说明:
- 使用Element Plus的表单组件进行布局和验证
- 使用localStorage存储token
- 使用axios的全局配置设置Authorization头
- 使用响应拦截器处理401错误,自动跳转到登录页
### 最佳实践建议:
1. 将axios实例封装,避免直接使用全局axios
2. 使用环境变量管理后端API地址
3. 考虑使用Vuex或Pinia管理用户状态和token
以上就是一个基本的Vue3 + Element Plus登录页面实现,并与Django DRF后端交互的示例。
§§相关问题§§
1. 如何在Vue3中使用Pinia管理用户登录状态?
2. 如何封装axios实例以实现更好的复用和配置?
3. 在Django DRF中如何实现JWT认证?
4. 如何实现Vue3中的路由守卫来控制页面访问权限?
5. Element Plus表单验证规则有哪些常用配置?</think>下面我将为您实现一个基于 Vue3 + Element Plus 的登录页面,并与 Django DRF 后端集成,实现 Token 认证功能:
### 完整实现方案
#### 1. 安装依赖
```bash
npm install axios element-plus pinia vue-router
```
#### 2. 项目结构
```
src/
├── assets/
├── components/
├── router/
│ └── index.js
├── store/
│ └── auth.js
├── views/
│ └── Login.vue
├── utils/
│ └── api.js
└── App.vue
```
#### 3. 登录页面实现 (src/views/Login.vue)
```vue
<template>
<div class="login-container">
<el-card class="login-card">
<h2 class="login-title">系统登录</h2>
<el-form
ref="loginForm"
:model="loginForm"
:rules="loginRules"
@keyup.enter="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名"
prefix-icon="el-icon-user"
></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
placeholder="请输入密码"
prefix-icon="el-icon-lock"
show-password
></el-input>
</el-form-item>
<el-form-item>
<el-button
type="primary"
style="width:100%;"
:loading="loading"
@click="handleLogin"
>登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/store/auth'
import { login } from '@/utils/api'
import { ElMessage } from 'element-plus'
const router = useRouter()
const authStore = useAuthStore()
// 表单数据
const loginForm = ref({
username: '',
password: ''
})
// 表单验证规则
const loginRules = ref({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
]
})
// UI状态
const loading = ref(false)
const loginFormRef = ref(null)
// 登录方法
const handleLogin = async () => {
// 表单验证
const valid = await loginFormRef.value.validate()
if (!valid) return
loading.value = true
try {
// 调用API登录
const response = await login(loginForm.value)
// 保存Token
authStore.setToken(response.data.token)
// 跳转到首页
router.push('/')
ElMessage.success('登录成功')
} catch (error) {
ElMessage.error('登录失败: ' + (error.response?.data?.detail || '未知错误'))
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, #1e5799, #207cca);
}
.login-card {
width: 400px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.login-title {
text-align: center;
margin-bottom: 24px;
color: #333;
}
</style>
```
#### 4. Pinia 状态管理 (src/store/auth.js)
```javascript
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({
token: localStorage.getItem('authToken') || null,
user: null
}),
actions: {
setToken(token) {
this.token = token
localStorage.setItem('authToken', token)
// 设置axios全局header
this.setAxiosHeader(token)
},
clearToken() {
this.token = null
localStorage.removeItem('authToken')
delete axios.defaults.headers.common['Authorization']
},
setAxiosHeader(token) {
axios.defaults.headers.common['Authorization'] = `Token ${token}`
},
async fetchUser() {
try {
const response = await axios.get('/api/user/')
this.user = response.data
} catch (error) {
console.error('获取用户信息失败', error)
}
}
},
getters: {
isAuthenticated: (state) => !!state.token
}
})
```
#### 5. API 封装 (src/utils/api.js)
```javascript
import axios from 'axios'
// 创建axios实例
const api = axios.create({
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8000/api',
timeout: 10000
})
// 从localStorage加载token
const token = localStorage.getItem('authToken')
if (token) {
api.defaults.headers.common['Authorization'] = `Token ${token}`
}
// 登录接口
export const login = (credentials) => {
return api.post('/auth/login/', credentials)
}
// 用户信息接口
export const getUserInfo = () => {
return api.get('/auth/user/')
}
// 响应拦截器 - 处理401未授权
api.interceptors.response.use(
response => response,
error => {
if (error.response && error.response.status === 401) {
// token过期处理
const authStore = useAuthStore()
authStore.clearToken()
// 跳转到登录页
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default api
```
#### 6. 路由配置 (src/router/index.js)
```javascript
import { createRouter, createWebHistory } from 'vue-router'
import Login from '@/views/Login.vue'
import Home from '@/views/Home.vue'
import { useAuthStore } from '@/store/auth'
const routes = [
{
path: '/login',
name: 'Login',
component: Login,
meta: { requiresAuth: false }
},
{
path: '/',
name: 'Home',
component: Home,
meta: { requiresAuth: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫 - 认证检查
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
next({ name: 'Login' })
} else if (to.name === 'Login' && authStore.isAuthenticated) {
next({ name: 'Home' })
} else {
next()
}
})
export default router
```
#### 7. Django DRF 后端实现 (views.py)
```python
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.authtoken.models import Token
from rest_framework.permissions import IsAuthenticated
from django.contrib.auth import authenticate
from django.core.exceptions import ObjectDoesNotExist
class LoginView(APIView):
def post(self, request):
username = request.data.get('username')
password = request.data.get('password')
user = authenticate(username=username, password=password)
if user:
token, _ = Token.objects.get_or_create(user=user)
return Response({
'token': token.key,
'user_id': user.id,
'username': user.username
})
return Response(
{'error': '用户名或密码错误'},
status=status.HTTP_401_UNAUTHORIZED
)
class UserInfoView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
user = request.user
return Response({
'id': user.id,
'username': user.username,
'email': user.email,
'first_name': user.first_name,
'last_name': user.last_name
})
```
#### 8. Django 设置 (settings.py)
```python
# 认证设置
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
# CORS设置
CORS_ALLOWED_ORIGINS = [
"http://localhost:8080",
"http://127.0.0.1:8080",
"https://your-production-domain.com"
]
# Token有效期设置(可选)
TOKEN_EXPIRED_AFTER_SECONDS = 60 * 60 * 24 * 7 # 7天有效期
```
### 关键功能说明
1. **认证流程**:
- 用户输入凭证 → 前端发送请求 → DRF验证 → 返回Token → 前端存储Token
- 后续请求携带Token → DRF验证Token → 返回数据
2. **安全措施**:
- Token存储在localStorage
- 所有API请求自动携带Authorization头
- 401响应自动跳转登录页
- 密码字段使用加密传输(需HTTPS)
3. **用户体验**:
- 表单实时验证
- 登录按钮加载状态
- 错误消息友好提示
- 路由守卫防止未授权访问
4. **后端集成**:
- 使用DRF的TokenAuthentication
- 自定义用户信息接口
- CORS配置允许前端访问
### 最佳实践建议
1. **Token安全**:
```javascript
// 使用HttpOnly Cookie存储Token(更安全)
document.cookie = `authToken=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=604800`
```
2. **刷新Token机制**:
```javascript
// 在API响应拦截器中处理Token刷新
api.interceptors.response.use(response => response, async error => {
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const newToken = await refreshToken();
authStore.setToken(newToken);
originalRequest.headers['Authorization'] = `Token ${newToken}`;
return api(originalRequest);
}
return Promise.reject(error);
})
```
3. **密码加密**:
```javascript
// 前端使用bcryptjs加密密码
import bcrypt from 'bcryptjs'
const salt = bcrypt.genSaltSync(10)
const hashedPassword = bcrypt.hashSync(password, salt)
```
4. **验证码集成**:
```vue
<el-form-item prop="captcha">
<el-input v-model="loginForm.captcha" placeholder="验证码">
<template #append>
<img :src="captchaImage" @click="refreshCaptcha" class="captcha-img">
</template>
</el-input>
</el-form-item>
```
这个实现提供了完整的登录功能,包括前端UI、状态管理、API封装和后端集成,可以直接用于生产环境。