feat(auth): 实现登录功能和认证逻辑

- 新增 auth.ts 文件实现登录接口调用
- 创建 auth store 管理认证状态
- 实现登录页面组件
- 优化订单列表页面样式
- 添加记住密码和自动登录功能
master
chenhao 2025-08-28 15:37:34 +08:00
parent 0752efd3ff
commit 411253a277
6 changed files with 445 additions and 15 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

20
src/api/auth.ts 100644
View File

@ -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)
}

80
src/store/auth.ts 100644
View File

@ -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
}
}
}
})

17
src/store/index.ts 100644
View File

@ -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()
}

View File

@ -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>

View File

@ -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>