登录功能
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 30s
Docker Build & Deploy / Deploy to Production (push) Successful in 5s

This commit is contained in:
孙诚
2025-12-25 13:27:23 +08:00
parent ebb49577dd
commit 728c39f43d
16 changed files with 395 additions and 23 deletions

View File

@@ -1,7 +1,7 @@
<template>
<van-config-provider :theme="theme">
<RouterView />
<van-tabbar v-model="active">
<van-tabbar v-model="active" v-show="showTabbar">
<van-tabbar-item icon="notes-o" to="/calendar">
日历
</van-tabbar-item>
@@ -20,10 +20,15 @@
<script setup>
import { RouterView, useRoute } from 'vue-router'
import { ref, onMounted, onUnmounted } from 'vue'
import { ref, onMounted, onUnmounted, computed } from 'vue'
const route = useRoute()
// 根据路由判断是否显示Tabbar
const showTabbar = computed(() => {
return route.path !== '/login'
})
const active = ref(0)
const theme = ref('light')

View File

@@ -1,5 +1,7 @@
import axios from 'axios'
import { showToast } from 'vant'
import { useAuthStore } from '@/stores/auth'
import router from '@/router'
// 创建 axios 实例
const request = axios.create({
@@ -13,11 +15,11 @@ const request = axios.create({
// 请求拦截器
request.interceptors.request.use(
config => {
// 可以在这里添加 token 认证信息
// const token = localStorage.getItem('token')
// if (token) {
// config.headers.Authorization = `Bearer ${token}`
// }
// 添加 token 认证信息
const authStore = useAuthStore()
if (authStore.token) {
config.headers.Authorization = `Bearer ${authStore.token}`
}
return config
},
error => {
@@ -53,6 +55,10 @@ request.interceptors.response.use(
break
case 401:
message = '未授权,请重新登录'
// 清除登录状态并跳转到登录页
const authStore = useAuthStore()
authStore.logout()
router.push({ name: 'login', query: { redirect: router.currentRoute.value.fullPath } })
break
case 403:
message = '拒绝访问'

View File

@@ -1,29 +1,57 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
name: 'login',
component: () => import('../views/LoginView.vue'),
meta: { requiresAuth: false },
},
{
path: '/',
name: 'transactions',
component: () => import('../views/TransactionsRecord.vue'),
meta: { requiresAuth: true },
},
{
path: '/email',
name: 'email',
component: () => import('../views/EmailRecord.vue'),
meta: { requiresAuth: true },
},
{
path: '/setting',
name: 'setting',
component: () => import('../views/SettingView.vue'),
meta: { requiresAuth: true },
},
{
path: '/calendar',
name: 'calendar',
component: () => import('../views/CalendarView.vue'),
meta: { requiresAuth: true },
},
],
})
// 路由守卫
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
const requiresAuth = to.meta.requiresAuth !== false // 默认需要认证
if (requiresAuth && !authStore.isAuthenticated) {
// 需要认证但未登录,跳转到登录页
next({ name: 'login', query: { redirect: to.fullPath } })
} else if (to.name === 'login' && authStore.isAuthenticated) {
// 已登录用户访问登录页,跳转到首页
next({ name: 'transactions' })
} else {
next()
}
})
export default router

49
Web/src/stores/auth.js Normal file
View File

@@ -0,0 +1,49 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import request from '@/api/request'
export const useAuthStore = defineStore('auth', () => {
const token = ref(localStorage.getItem('token') || '')
const expiresAt = ref(localStorage.getItem('expiresAt') || '')
const isAuthenticated = computed(() => {
if (!token.value || !expiresAt.value) return false
// 检查token是否过期
return new Date(expiresAt.value) > new Date()
})
const login = async (password) => {
try {
const response = await request.post('/Auth/Login', { password })
const { token: newToken, expiresAt: newExpiresAt } = response.data
token.value = newToken
expiresAt.value = newExpiresAt
localStorage.setItem('token', newToken)
localStorage.setItem('expiresAt', newExpiresAt)
return true
} catch (error) {
if (error.response?.status === 401) {
throw new Error('密码错误')
}
throw new Error('登录失败,请稍后重试')
}
}
const logout = () => {
token.value = ''
expiresAt.value = ''
localStorage.removeItem('token')
localStorage.removeItem('expiresAt')
}
return {
token,
expiresAt,
isAuthenticated,
login,
logout,
}
})

107
Web/src/views/LoginView.vue Normal file
View File

@@ -0,0 +1,107 @@
<template>
<div class="login-container">
<div class="login-box">
<h1 class="login-title">账单</h1>
<div class="login-form">
<van-field
v-model="password"
type="password"
placeholder="请输入密码"
clearable
:border="true"
@keyup.enter="handleLogin"
>
<template #left-icon>
<van-icon name="lock" />
</template>
</van-field>
<van-button
type="primary"
block
round
:loading="loading"
@click="handleLogin"
class="login-button"
>
登录
</van-button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
const authStore = useAuthStore()
const password = ref('')
const loading = ref(false)
const handleLogin = async () => {
if (!password.value) {
showToast('请输入密码')
return
}
loading.value = true
try {
await authStore.login(password.value)
showToast({ type: 'success', message: '登录成功' })
router.push('/')
} catch (error) {
showToast({ type: 'fail', message: error.message || '登录失败' })
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login-container {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-box {
width: 90%;
max-width: 400px;
border-radius: 20px;
padding: 40px 30px;
}
.login-title {
text-align: center;
font-size: 28px;
font-weight: bold;
margin-bottom: 40px;
}
.login-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.login-button {
margin-top: 10px;
height: 44px;
font-size: 16px;
}
:deep(.van-field) {
border-radius: 12px;
padding: 12px 16px;
}
:deep(.van-field__control) {
font-size: 15px;
}
</style>

View File

@@ -18,14 +18,25 @@
<van-cell-group inset>
<van-cell title="智能分类" is-link />
</van-cell-group>
<div class="detail-header">
<p>账户</p>
</div>
<van-cell-group inset>
<van-cell title="退出登录" is-link @click="handleLogout" />
</van-cell-group>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { showLoadingToast, showSuccessToast, showToast, closeToast } from 'vant'
import { useRouter } from 'vue-router'
import { showLoadingToast, showSuccessToast, showToast, closeToast, showConfirmDialog } from 'vant'
import { uploadBillFile } from '@/api/billImport'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
const authStore = useAuthStore()
const fileInputRef = ref(null)
const currentType = ref('')
@@ -89,6 +100,25 @@ const handleFileChange = async (event) => {
event.target.value = ''
}
}
/**
* 处理退出登录
*/
const handleLogout = async () => {
try {
await showConfirmDialog({
title: '提示',
message: '确定要退出登录吗?',
})
authStore.logout()
showSuccessToast('已退出登录')
router.push({ name: 'login' })
} catch (error) {
console.error('取消退出登录:', error)
showToast('已取消退出登录')
}
}
</script>
<style scoped>