登录接口

pull/57/head
ZHW 1 month ago
parent 05a12d7143
commit cd3e019cd6

@ -11,6 +11,7 @@ import com.campus.water.entity.dto.request.LoginRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.util.UUID;
@ -38,7 +39,7 @@ public class LoginService {
};
}
private LoginVO handleAdminLogin(String username, String password) {
/* private LoginVO handleAdminLogin(String username, String password) {
Admin admin = adminRepository.findByAdminName(username)
.orElseThrow(() -> new RuntimeException("管理员不存在"));
@ -47,8 +48,29 @@ public class LoginService {
}
return createLoginVO(admin.getAdminId(), username, "admin");
}*/
private LoginVO handleAdminLogin(String username, String password) {
Admin admin = adminRepository.findByAdminName(username)
.orElseThrow(() -> new RuntimeException("管理员不存在"));
boolean matches;
// 临时支持 MD5 验证(仅用于测试环境)
if (admin.getPassword().startsWith("$2a$") || admin.getPassword().startsWith("$2y$")) {
// BCrypt 格式密码
matches = passwordEncoder.matches(password, admin.getPassword());
} else {
// MD5 格式密码
String md5Password = DigestUtils.md5DigestAsHex(password.getBytes());
matches = md5Password.equals(admin.getPassword());
}
if (!matches) {
throw new RuntimeException("密码错误");
}
return createLoginVO(admin.getAdminId(), username, "admin");
}
private LoginVO handleUserLogin(String username, String password) {
UserPO user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在"));

@ -0,0 +1,2 @@
VITE_API_BASE_URL=http://localhost:8080
VITE_APP_ORIGIN=http://localhost:5173

@ -1,58 +1,61 @@
import type { LoginRequest, LoginVO } from './types/auth'
// 替换原文件内容
import type { LoginRequest, LoginResponse, LoginVO } from './types/auth'
// 真实的登录API调用
export const realLoginApi = async (data: LoginRequest): Promise<LoginVO> => {
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'
console.log('🌐 调用登录接口:', `${API_BASE_URL}/api/common/login`)
console.log('📤 请求数据:', data)
try {
const response = await fetch(`${API_BASE_URL}/api/common/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
console.log('📥 响应状态:', response.status, response.statusText)
if (!response.ok) {
throw new Error(`网络请求失败: ${response.status} ${response.statusText}`)
export const realLoginApi = async (data: LoginRequest): Promise<LoginResponse> => {
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'
console.log('🌐 调用登录接口:', `${API_BASE_URL}/api/common/login`)
console.log('📤 请求数据:', data)
try {
const response = await fetch(`${API_BASE_URL}/api/common/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
console.log('📥 响应状态:', response.status, response.statusText)
if (!response.ok) {
const errorText = await response.text()
console.error('❌ 响应内容:', errorText)
throw new Error(`网络请求失败: ${response.status} ${response.statusText}`)
}
const result: LoginResponse = await response.json()
console.log('✅ 登录响应:', result)
return result
} catch (error: any) {
console.error('❌ 登录接口调用失败:', error)
throw new Error(`登录失败: ${error.message}`)
}
const result: LoginVO = await response.json()
console.log('✅ 登录响应:', result)
return result
} catch (error: any) {
console.error('❌ 登录接口调用失败:', error)
throw new Error(`登录失败: ${error.message}`)
}
}
// 备用模拟登录
export const mockLoginApi = async (data: LoginRequest): Promise<LoginVO> => {
await new Promise(resolve => setTimeout(resolve, 1000))
if (data.username === 'admin' && data.password === '123456') {
return {
code: 200,
message: '登录成功',
data: {
token: 'mock-jwt-token-' + Date.now(),
userInfo: {
id: 1,
username: 'admin',
realName: '张管理员',
role: 'admin',
avatar: ''
export const mockLoginApi = async (data: LoginRequest): Promise<LoginResponse> => {
await new Promise(resolve => setTimeout(resolve, 1000))
if (data.username === 'admin' && data.password === '123456') {
return {
code: 200,
message: '登录成功',
data: {
token: 'mock-jwt-token-' + Date.now(),
userInfo: {
id: 1,
username: 'admin',
realName: '张管理员',
role: 'admin',
avatar: ''
}
}
}
}
} else {
throw new Error('用户名或密码错误')
}
} else {
throw new Error('用户名或密码错误')
}
}

@ -0,0 +1,15 @@
// src/api/modules/auth.ts
import { api } from '../request'
import type { LoginRequest, LoginVO, ResultVO } from '../types/auth'
class AuthApi {
/**
*
*/
async login(data: LoginRequest): Promise<ResultVO<LoginVO>> {
console.log('🚀 调用登录接口:', data)
return api.post<ResultVO<LoginVO>>('/api/common/login', data)
}
}
export const authApi = new AuthApi()

@ -0,0 +1,141 @@
// src/api/request.ts
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'
// 统一的 fetch 封装
export async function request<T>(
url: string,
options: RequestInit = {}
): Promise<T> {
console.log(`🌐 发送请求: ${API_BASE_URL}${url}`, {
method: options.method || 'GET',
headers: options.headers,
body: options.body ? JSON.parse(options.body as string) : undefined,
})
const defaultOptions: RequestInit = {
headers: {
'Content-Type': 'application/json',
...(options.headers || {}),
},
}
// 确保登录请求不携带任何认证信息
const isLoginRequest = url.includes('/login')
if (isLoginRequest) {
console.log('🔐 这是登录请求,不携带认证头')
// 确保没有 Authorization header
const headers = new Headers(defaultOptions.headers)
headers.delete('Authorization')
headers.delete('authorization')
defaultOptions.headers = headers
} else {
// 非登录请求,从存储中获取 token
const token = localStorage.getItem('token') || sessionStorage.getItem('token')
if (token) {
const headers = new Headers(defaultOptions.headers)
headers.set('Authorization', `Bearer ${token}`)
defaultOptions.headers = headers
}
}
try {
const response = await fetch(`${API_BASE_URL}${url}`, {
...defaultOptions,
...options,
})
console.log('📥 响应状态:', response.status, response.statusText)
// 尝试读取响应文本(无论成功与否)
let responseText = ''
try {
responseText = await response.text()
console.log('📥 响应内容:', responseText)
} catch (e) {
console.log('📥 无法读取响应文本')
}
if (!response.ok) {
let errorMessage = `HTTP ${response.status}: ${response.statusText}`
if (responseText) {
try {
const errorJson = JSON.parse(responseText)
errorMessage = errorJson.message || errorJson.error || errorMessage
} catch {
errorMessage = `${errorMessage}\n${responseText}`
}
}
console.error('❌ 请求失败:', errorMessage)
throw new Error(errorMessage)
}
// 尝试解析 JSON
if (responseText) {
try {
const data = JSON.parse(responseText)
console.log('✅ 解析成功的数据:', data)
return data
} catch (e) {
console.error('❌ JSON 解析失败:', e)
throw new Error(`响应不是有效的 JSON: ${responseText}`)
}
} else {
// 没有响应体的情况(如 204 No Content
return {} as T
}
} catch (error: any) {
console.error('❌ 请求异常:', error)
// 处理网络错误
if (error.name === 'TypeError' && error.message.includes('fetch')) {
throw new Error('网络连接失败,请检查网络设置和后端服务')
}
throw error
}
}
// 封装常用 HTTP 方法
export const api = {
get<T>(url: string) {
return request<T>(url, { method: 'GET' })
},
post<T>(url: string, data?: any) {
return request<T>(url, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
})
},
put<T>(url: string, data?: any) {
return request<T>(url, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
})
},
patch<T>(url: string, data?: any) {
return request<T>(url, {
method: 'PATCH',
body: data ? JSON.stringify(data) : undefined,
})
},
delete<T>(url: string) {
return request<T>(url, { method: 'DELETE' })
},
upload<T>(url: string, formData: FormData) {
const headers = new Headers()
// 上传文件时不要设置 Content-Type浏览器会自动设置
headers.delete('Content-Type')
return request<T>(url, {
method: 'POST',
headers,
body: formData,
})
},
}

@ -1,22 +1,29 @@
// 与后端 LoginRequest 对应的类型
// src/api/types/auth.ts
// 登录请求参数 - 匹配后端的 LoginRequest
export interface LoginRequest {
username: string
password: string
rememberMe?: boolean
username: string
password: string
userType: string // 添加这个属性
rememberMe?: boolean
}
// 通用响应结构
export interface ResultVO<T = any> {
code: number
message: string
data: T
}
// 与后端 LoginVO 对应的类型
// 登录响应数据 - 匹配后端的 LoginVO
export interface LoginVO {
code: number
message: string
data: {
token: string
userInfo: {
id: number
username: string
realName: string
role: string
avatar?: string
id: number
username: string
realName?: string // 根据后端字段调整
role?: string // 根据后端字段调整
userType?: string // 添加这个字段
avatar?: string
}
}
}

@ -4,7 +4,7 @@
<h1 class="system-title">校园矿化水系统</h1>
</div>
<div class="header-right">
<div class="user-info">
<div class="user-info" @click="goToProfile">
<div class="user-avatar">
<img :src="userAvatar" alt="用户头像" />
</div>
@ -17,8 +17,15 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const userAvatar = ref('images/用户界面/u106.jpg') //
const router = useRouter()
//
const goToProfile = () => {
router.push('/home/profile')
}
</script>
<style scoped>

@ -29,15 +29,18 @@ interface MenuItem {
const route = useRoute()
const router = useRouter()
// ID
const activeItem = computed(() => {
const currentPath = route.path
//
const item = menuItems.find(item =>
item.route === currentPath ||
item.children?.some(child => child.route === currentPath)
)
return item?.id || 1
return item?.id || 1 //
})
//
const menuItems: MenuItem[] = [
{
id: 1,
@ -72,7 +75,7 @@ const menuItems: MenuItem[] = [
id: 4,
name: '人员管理',
icon: '👥',
route: '/home/personnel',
//
children: [
{ name: '管理员', route: '/home/personnel/admin' },
{ name: '维修人员', route: '/home/personnel/maintenance' },
@ -83,7 +86,7 @@ const menuItems: MenuItem[] = [
id: 5,
name: '片区',
icon: '🗺️',
route: '/home/area',
//
children: [
{ name: '市区', route: '/home/area/urban' },
{ name: '校区', route: '/home/area/campus' }
@ -97,6 +100,7 @@ const menuItems: MenuItem[] = [
}
]
//
const handleItemClick = (itemId: number) => {
const item = menuItems.find(m => m.id === itemId)
if (item?.route) {
@ -111,6 +115,8 @@ const handleItemClick = (itemId: number) => {
background: white;
border-right: 1px solid #e1e8ed;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.05);
height: calc(100vh - 60px); /* 减去头部高度 */
overflow-y: auto; /* 菜单过多时可滚动 */
}
.sidebar-nav {

@ -1,33 +1,251 @@
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { createRouter, createWebHistory, type NavigationGuardNext, type RouteLocationNormalized } from 'vue-router'
import LoginView from '../views/LoginView.vue'
import MainLayout from '../components/layout/MainLayout.vue' // 导入布局组件
import MainLayout from '../components/layout/MainLayout.vue'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'login',
component: LoginView
},
{
path: '/home',
component: MainLayout, // 使用 MainLayout 作为布局
children: [
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '',
name: 'home',
component: () => import('../views/Dashboard.vue') // Dashboard 作为子路由
}
]
},
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue')
},
],
path: '/',
name: 'login',
component: LoginView,
meta: {
title: '登录',
requiresAuth: false // 不需要认证
}
},
{
path: '/home',
component: MainLayout,
meta: {
requiresAuth: true // 需要认证
},
children: [
{
path: '',
name: 'home',
component: () => import('../views/Dashboard.vue'),
meta: {
title: '首页'
}
},
// 设备监控相关路由
{
path: 'equipment',
name: 'equipment',
component: () => import('../views/equipment/EquipmentView.vue'),
meta: {
title: '设备监控'
}
},
{
path: 'equipment/water-maker',
name: 'water-maker',
component: () => import('../views/equipment/WaterMaker.vue'),
meta: {
title: '制水设备'
}
},
{
path: 'equipment/water-supplier',
name: 'water-supplier',
component: () => import('../views/equipment/WaterSupplier.vue'),
meta: {
title: '供水设备'
}
},
// 工单管理相关路由
{
path: 'work-order',
name: 'work-order',
component: () => import('../views/workorder/WorkOrderView.vue'),
meta: {
title: '工单管理'
}
},
{
path: 'work-order/pending',
name: 'work-order-pending',
component: () => import('../views/workorder/Pending.vue'),
meta: {
title: '待处理工单'
}
},
{
path: 'work-order/timeout',
name: 'work-order-timeout',
component: () => import('../views/workorder/Timeout.vue'),
meta: {
title: '超时工单'
}
},
{
path: 'work-order/processing',
name: 'work-order-processing',
component: () => import('../views/workorder/Processing.vue'),
meta: {
title: '处理中工单'
}
},
{
path: 'work-order/review',
name: 'work-order-review',
component: () => import('../views/workorder/Review.vue'),
meta: {
title: '待审核工单'
}
},
{
path: 'work-order/review/:id',
name: 'ReviewDetail',
component: () => import('../views/workorder/ReviewDetail.vue'),
meta: {
title: '工单审核详情'
}
},
{
path: 'work-order/completed',
name: 'work-order-completed',
component: () => import('../views/workorder/Completed.vue'),
meta: {
title: '已完成工单'
}
},
{
path: '/home/work-order/completed/:id',
name: 'CompletedDetail',
component: () => import('@/views/workorder/CompletedDetail.vue'),
meta: {
title: '结单信息'
}
},
// 人员管理相关路由
{
path: 'personnel/admin',
name: 'personnel-admin',
component: () => import('../views/personnel/Admin.vue'),
meta: {
title: '管理员管理'
}
},
{
path: 'personnel/maintenance',
name: 'personnel-maintenance',
component: () => import('../views/personnel/Maintenance.vue'),
meta: {
title: '运维人员管理'
}
},
{
path: 'personnel/user',
name: 'personnel-user',
component: () => import('../views/personnel/User.vue'),
meta: {
title: '用户管理'
}
},
// 片区相关路由
{
path: 'area/urban',
name: 'area-urban',
component: () => import('../views/area/Urban.vue'),
meta: {
title: '城市片区'
}
},
{
path: 'equipment/water-maker/:id',
name: 'water-maker-detail',
component: () => import('../views/equipment/WaterMakerDetail.vue'),
meta: {
title: '制水设备详情'
}
},
{
path: 'area/campus',
name: 'area-campus',
component: () => import('../views/area/Campus.vue'),
meta: {
title: '校园片区'
}
},
// 个人信息路由
{
path: 'profile',
name: 'profile',
component: () => import('../views/Profile.vue'),
meta: {
title: '个人信息',
requiresAuth: true
}
}
]
},
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue'),
meta: {
title: '关于',
requiresAuth: false
}
},
/* // 404
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../views/NotFound.vue'),
meta: {
title: '页面不存在',
requiresAuth: false
}
}*/
]
})
// 路由守卫
router.beforeEach(async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
// 设置页面标题
const title = to.meta?.title as string || '校园直饮水管理系统'
document.title = title
// 获取认证状态
const authStore = useAuthStore()
// 初始化登录状态
if (!authStore.isLoggedIn) {
authStore.initialize()
}
// 判断是否需要认证
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
// 如果路由需要认证但用户未登录
if (requiresAuth && !authStore.isLoggedIn) {
// 重定向到登录页面,并保存当前想要访问的路径
next({
name: 'login',
query: { redirect: to.fullPath }
})
}
// 如果用户已登录但访问登录页面
else if (to.name === 'login' && authStore.isLoggedIn) {
// 检查是否有重定向路径
const redirect = from.query.redirect as string || '/home'
next(redirect)
}
// 其他情况正常放行
else {
next()
}
})
// 路由后置守卫 - 可在这里添加一些统计或清理工作
router.afterEach((to: RouteLocationNormalized, from: RouteLocationNormalized) => {
// 可以在这里添加页面访问统计等
console.log(`路由跳转: ${from.fullPath} -> ${to.fullPath}`)
})
export default router

@ -1,71 +1,105 @@
// src/stores/auth.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { realLoginApi } from '@/api/auth'
import type { LoginVO } from '@/api/types/auth'
import { authApi } from '@/api/modules/auth'
import type { LoginRequest, LoginVO, ResultVO } from '@/api/types/auth'
interface UserInfo {
id: number
username: string
realName: string
role: string
avatar?: string
id: number
username: string
realName: string
role: string
avatar?: string
}
export const useAuthStore = defineStore('auth', () => {
const token = ref<string>('')
const userInfo = ref<UserInfo | null>(null)
const isLoggedIn = ref(false)
const token = ref<string>('')
const userInfo = ref<UserInfo | null>(null)
const isLoggedIn = ref(false)
// 登录方法
const login = async (loginData: { username: string; password: string; rememberMe?: boolean }) => {
try {
const response: LoginVO = await realLoginApi(loginData)
if (response.code === 200) {
token.value = response.data.token
userInfo.value = response.data.userInfo
isLoggedIn.value = true
// 存储到localStorage
localStorage.setItem('token', response.data.token)
localStorage.setItem('userInfo', JSON.stringify(response.data.userInfo))
return response
} else {
throw new Error(response.message || '登录失败')
}
} catch (error: any) {
throw new Error(error.message || '登录失败,请检查网络连接')
// 真实登录接口调用
const login = async (loginData: LoginRequest) => {
try {
// 调用真实后端接口
const response: ResultVO<LoginVO> = await authApi.login(loginData)
// 检查响应状态
if (response.code !== 200) {
throw new Error(response.message || '登录失败')
}
// 保存 token 和用户信息
token.value = response.data.token
userInfo.value = response.data.userInfo
isLoggedIn.value = true
// 存储到 localStorage如果用户选择记住我
if (loginData.rememberMe) {
localStorage.setItem('token', response.data.token)
localStorage.setItem('userInfo', JSON.stringify(response.data.userInfo))
localStorage.setItem('rememberMe', 'true')
} else {
sessionStorage.setItem('token', response.data.token)
sessionStorage.setItem('userInfo', JSON.stringify(response.data.userInfo))
localStorage.removeItem('rememberMe')
}
return response
} catch (error: any) {
console.error('登录失败:', error)
throw error
}
}
// 退出登录
const logout = () => {
// 可以调用后端登出接口
// authApi.logout().catch(console.error)
// 清除本地存储
token.value = ''
userInfo.value = null
isLoggedIn.value = false
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
localStorage.removeItem('rememberMe')
sessionStorage.removeItem('token')
sessionStorage.removeItem('userInfo')
}
}
// 退出登录
const logout = () => {
token.value = ''
userInfo.value = null
isLoggedIn.value = false
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
}
// 初始化时从存储恢复状态
const initialize = () => {
const rememberMe = localStorage.getItem('rememberMe')
const storage = rememberMe ? localStorage : sessionStorage
const savedToken = storage.getItem('token')
const savedUserInfo = storage.getItem('userInfo')
// 初始化时从localStorage恢复状态
const initialize = () => {
const savedToken = localStorage.getItem('token')
const savedUserInfo = localStorage.getItem('userInfo')
if (savedToken && savedUserInfo) {
token.value = savedToken
userInfo.value = JSON.parse(savedUserInfo)
isLoggedIn.value = true
if (savedToken && savedUserInfo) {
try {
token.value = savedToken
userInfo.value = JSON.parse(savedUserInfo)
isLoggedIn.value = true
} catch (e) {
console.error('恢复登录状态失败:', e)
logout()
}
}
}
}
return {
token,
userInfo,
isLoggedIn,
login,
logout,
initialize
}
// 检查是否已登录(用于路由守卫)
const checkAuth = (): boolean => {
return isLoggedIn.value && !!token.value
}
return {
token,
userInfo,
isLoggedIn,
login,
logout,
initialize,
checkAuth,
}
})

@ -7,6 +7,22 @@
</div>
<form class="login-form" @submit.prevent="handleLogin">
<!-- 用户类型选择 -->
<div class="form-group">
<label for="userType">用户类型</label>
<select
id="userType"
v-model="loginForm.userType"
class="form-input"
:disabled="loading"
required
>
<option value="user">普通用户</option>
<option value="admin">管理员</option>
<option value="repairer">维修人员</option>
</select>
</div>
<div class="form-group">
<label for="username">用户名</label>
<input
@ -68,9 +84,11 @@ const debugInfo = ref('')
const loginForm = reactive({
username: '',
password: '',
userType: 'user', //
rememberMe: false
})
// handleLogin
const handleLogin = async () => {
if (!loginForm.username.trim() || !loginForm.password.trim()) {
alert('请输入用户名和密码')
@ -78,27 +96,34 @@ const handleLogin = async () => {
}
loading.value = true
debugInfo.value = '开始登录...'
try {
debugInfo.value = `调用接口: ${import.meta.env.VITE_API_BASE_URL}/api/common/login\n请求数据: ${JSON.stringify(loginForm, null, 2)}`
//
//
await authStore.login({
username: loginForm.username,
password: loginForm.password,
userType: loginForm.userType, //
rememberMe: loginForm.rememberMe
})
debugInfo.value += '\n✅ 登录成功,跳转到首页...'
//
router.push('/home')
//
const redirect = router.currentRoute.value.query.redirect as string
if (redirect) {
router.push(redirect)
} else {
router.push('/home')
}
} catch (error: any) {
console.error('登录失败:', error)
debugInfo.value += `\n❌ 登录失败: ${error.message}`
alert(error.message || '登录失败,请检查用户名和密码')
//
const errorMessage = error.message.includes('Network')
? '网络连接失败,请检查网络设置'
: error.message.includes('401')
? '用户名或密码错误'
: error.message || '登录失败,请稍后重试'
alert(errorMessage)
} finally {
loading.value = false
}
@ -242,4 +267,4 @@ const handleLogin = async () => {
white-space: pre-wrap;
word-wrap: break-word;
}
</style>
</style>

@ -6,7 +6,11 @@
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
"@/*": ["./src/*"],
"lib": ["ES2015", "DOM"], // ES2015 Promise
"target": "ES2015", // ES2015
"module": "ESNext", //
"moduleResolution": "NodeNext"
}
}
}
Loading…
Cancel
Save