feat(auth): 实现登录功能和认证逻辑
- 新增 auth.ts 文件实现登录接口调用 - 创建 auth store 管理认证状态 - 实现登录页面组件 - 优化订单列表页面样式 - 添加记住密码和自动登录功能master
parent
0752efd3ff
commit
411253a277
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -0,0 +1,20 @@
|
|||
import http from '@/utils/http'
|
||||
import type { ApiResponse, LoginParams } from '@/types'
|
||||
import type { AxiosResponse } from 'axios'
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
export const login = (params: LoginParams): Promise<AxiosResponse<ApiResponse<{
|
||||
token: string
|
||||
userInfo: any
|
||||
}>>> => {
|
||||
// 创建FormData对象
|
||||
const formData = new FormData()
|
||||
formData.append('username', params.username)
|
||||
formData.append('password', params.password)
|
||||
// 添加rememberMe参数,默认为true
|
||||
formData.append('rememberMe', String(params.rememberMe ?? true))
|
||||
|
||||
return http.post('/login', formData)
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { login } from '@/api/auth'
|
||||
import type { LoginParams } from '@/types'
|
||||
|
||||
interface AuthState {
|
||||
token: string | null
|
||||
userInfo: any | null
|
||||
isAuthenticated: boolean
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: (): AuthState => ({
|
||||
token: null,
|
||||
userInfo: null,
|
||||
isAuthenticated: localStorage.getItem('isAuthenticated') === 'true'
|
||||
}),
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
async login(params: LoginParams) {
|
||||
try {
|
||||
// 调用登录接口
|
||||
const response = await login(params)
|
||||
|
||||
// 检查响应状态
|
||||
if (response.status === 200 && response.data.code === 0) {
|
||||
// 由于服务器使用session判断登录状态,我们只需要设置认证状态为true
|
||||
this.isAuthenticated = true
|
||||
|
||||
// 保存到localStorage,用于页面刷新后保持登录状态
|
||||
localStorage.setItem('isAuthenticated', 'true')
|
||||
|
||||
// 如果选择了记住密码,则保存用户名和密码
|
||||
if (params.rememberMe) {
|
||||
localStorage.setItem('savedUsername', params.username)
|
||||
localStorage.setItem('savedPassword', params.password)
|
||||
} else {
|
||||
// 清除保存的用户名和密码
|
||||
localStorage.removeItem('savedUsername')
|
||||
localStorage.removeItem('savedPassword')
|
||||
}
|
||||
|
||||
return response.data
|
||||
} else {
|
||||
throw new Error(response.data.msg || '登录失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('登录接口调用失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
logout() {
|
||||
this.token = null
|
||||
this.userInfo = null
|
||||
this.isAuthenticated = false
|
||||
|
||||
// 清除localStorage中的认证状态
|
||||
localStorage.removeItem('isAuthenticated')
|
||||
|
||||
// 注意:这里不清除保存的用户名和密码,以便下次自动填充
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查登录状态
|
||||
*/
|
||||
checkAuthStatus() {
|
||||
// 从localStorage检查认证状态
|
||||
const isAuthenticated = localStorage.getItem('isAuthenticated')
|
||||
if (isAuthenticated === 'true') {
|
||||
this.isAuthenticated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { createPinia } from 'pinia'
|
||||
import { useAuthStore } from './auth'
|
||||
import { useOrderStore } from './order'
|
||||
|
||||
// 创建 pinia 实例
|
||||
export const store = createPinia()
|
||||
|
||||
// 导出所有 store 的 hook
|
||||
export { useAuthStore, useOrderStore }
|
||||
|
||||
// 初始化 store
|
||||
export const initStores = () => {
|
||||
// 可以在这里添加初始化逻辑
|
||||
const authStore = useAuthStore(store)
|
||||
// 检查本地存储的认证状态
|
||||
authStore.checkAuthStatus()
|
||||
}
|
||||
|
|
@ -134,18 +134,39 @@ onMounted(() => {
|
|||
<style lang="scss" scoped>
|
||||
.order-list-page {
|
||||
min-height: 100vh;
|
||||
background-color: var(--background-color-secondary);
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.order-item {
|
||||
background: var(--background-color-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
padding: var(--spacing-lg);
|
||||
background: white;
|
||||
margin-bottom: 16px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
transition: all 0.2s;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: linear-gradient(90deg, #1989fa, #07c160);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--background-color-tertiary);
|
||||
transform: translateY(2px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -153,19 +174,43 @@ onMounted(() => {
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.order-code {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 12px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-primary);
|
||||
|
||||
&.pending {
|
||||
background-color: #FFF7E6;
|
||||
color: #FA8C16;
|
||||
}
|
||||
|
||||
&.approved {
|
||||
background-color: #F6FFED;
|
||||
color: #52C41A;
|
||||
}
|
||||
|
||||
&.rejected {
|
||||
background-color: #FFF2F0;
|
||||
color: #FF4D4F;
|
||||
}
|
||||
}
|
||||
|
||||
.order-info {
|
||||
.info-row {
|
||||
display: flex;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
|
|
@ -173,26 +218,51 @@ onMounted(() => {
|
|||
|
||||
.label {
|
||||
width: 80px;
|
||||
color: var(--text-color-secondary);
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
color: var(--text-color-primary);
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
|
||||
&.amount {
|
||||
color: var(--error-color);
|
||||
font-weight: 500;
|
||||
color: #FF6600;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 60px var(--spacing-lg);
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
|
||||
:deep(.van-empty) {
|
||||
.van-empty__image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.van-empty__description {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索栏样式优化
|
||||
:deep(.van-search) {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
|
||||
.van-search__content {
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<div class="logo-placeholder">
|
||||
<svg class="logo-icon" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="40" fill="#1989fa" />
|
||||
<path d="M30,50 L45,65 L70,35" stroke="white" stroke-width="8" fill="none" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="login-title">OMS</h1>
|
||||
<p class="login-subtitle">Order Management System</p>
|
||||
</div>
|
||||
|
||||
<van-form @submit="onSubmit" class="login-form">
|
||||
<van-cell-group inset>
|
||||
<van-field
|
||||
v-model="username"
|
||||
name="username"
|
||||
label="用户名"
|
||||
placeholder="请输入用户名"
|
||||
:rules="[{ required: true, message: '请填写用户名' }]"
|
||||
class="login-input"
|
||||
>
|
||||
<template #left-icon>
|
||||
<svg class="input-icon" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="35" r="15" fill="none" stroke="#1989fa" stroke-width="8" />
|
||||
<path d="M25,85 Q50,65 75,85" stroke="#1989fa" stroke-width="8" fill="none" />
|
||||
</svg>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<van-field
|
||||
v-model="password"
|
||||
type="password"
|
||||
name="password"
|
||||
label="密码"
|
||||
placeholder="请输入密码"
|
||||
:rules="[{ required: true, message: '请填写密码' }]"
|
||||
class="login-input"
|
||||
>
|
||||
<template #left-icon>
|
||||
<svg class="input-icon" viewBox="0 0 100 100">
|
||||
<rect x="20" y="40" width="60" height="40" rx="5" fill="none" stroke="#1989fa" stroke-width="8" />
|
||||
<circle cx="50" cy="25" r="10" fill="#1989fa" />
|
||||
</svg>
|
||||
</template>
|
||||
</van-field>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="login-options">
|
||||
<van-checkbox v-model="rememberMe" name="rememberMe" checked-color="#1989fa">
|
||||
记住密码
|
||||
</van-checkbox>
|
||||
<a href="javascript:void(0)" class="forgot-password">忘记密码?</a>
|
||||
</div>
|
||||
|
||||
<div class="login-button-container">
|
||||
<van-button round block type="primary" native-type="submit" :loading="loading" class="login-button">
|
||||
登录
|
||||
</van-button>
|
||||
</div>
|
||||
</van-form>
|
||||
|
||||
<div class="login-footer">
|
||||
<p class="footer-text">© 2025 订单管理系统 - 安全可靠的订单管理平台</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showFailToast, showSuccessToast } from 'vant'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import type { LoginParams } from '@/types'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 表单数据
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const rememberMe = ref(true)
|
||||
const loading = ref(false)
|
||||
|
||||
// 登录提交
|
||||
const onSubmit = async (values: LoginParams) => {
|
||||
loading.value = true
|
||||
try {
|
||||
// 调用登录接口
|
||||
await authStore.login(values)
|
||||
|
||||
// 显示成功提示
|
||||
showSuccessToast('登录成功')
|
||||
|
||||
// 跳转到列表页面
|
||||
router.push('/list')
|
||||
} catch (error: any) {
|
||||
console.error('登录失败:', error)
|
||||
showFailToast(error.message || '登录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时检查是否保存了用户名和密码
|
||||
import { onMounted } from 'vue'
|
||||
onMounted(() => {
|
||||
const savedUsername = localStorage.getItem('savedUsername')
|
||||
const savedPassword = localStorage.getItem('savedPassword')
|
||||
|
||||
if (savedUsername) {
|
||||
username.value = savedUsername
|
||||
}
|
||||
|
||||
if (savedPassword) {
|
||||
password.value = savedPassword
|
||||
rememberMe.value = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 40px 30px;
|
||||
box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #1989fa, #07c160, #ff6600);
|
||||
}
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.logo-placeholder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-input {
|
||||
margin-bottom: 15px;
|
||||
|
||||
:deep(.van-field__left-icon) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.login-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
margin: 10px 0 25px;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
color: #1989fa;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.login-button-container {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue