feat:登录、退出

This commit is contained in:
zhouwentao 2025-12-18 21:35:06 +08:00
parent 5bae1f8c12
commit 6c16860fae
9 changed files with 108 additions and 59 deletions

17
.vscode/settings.json vendored
View File

@ -1,5 +1,20 @@
{
"cSpell.words": ["Vitesse", "Vite", "unocss", "vitest", "vueuse", "pinia", "demi", "antfu", "iconify", "intlify", "vitejs", "unplugin", "pnpm"],
"cSpell.words": [
"antfu",
"demi",
"iconify",
"intlify",
"pinia",
"pnpm",
"realname",
"unocss",
"unplugin",
"Vite",
"vitejs",
"Vitesse",
"vitest",
"vueuse"
],
"i18n-ally.sourceLanguage": "zh-CN",
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": "locales",

18
prettier.config.mjs Normal file
View File

@ -0,0 +1,18 @@
/**
* @see https://prettier.io/docs/configuration
* @type {import("prettier").Config}
*/
const config = {
printWidth: 120,
tabWidth: 2,
useTabs: false,
semi: false,
singleQuote: true,
quoteProps: "as-needed",
bracketSpacing: true,
arrowParens: "avoid",
htmlWhitespaceSensitivity: "ignore",
bracketSameLine: true,
};
export default config;

BIN
public/beian.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -113,7 +113,7 @@ async function toggleLocales() {
</p>
<div class="flex items-center gap-2">
<img src="https://beian.miit.gov.cn/favicon.ico" alt="备案图标" class="h-4 w-4">
<img src="beian.ico" alt="备案图标" class="h-4 w-4">
<a href="http://beian.miit.gov.cn/" target="_blank" class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300">豫ICP备2024048033号</a>
</div>

View File

@ -14,6 +14,9 @@ const isMobileMenuOpen = ref(false)
const username = ref('')
const password = ref('')
const error = ref('')
//
const router = useRouter()
//
const route = useRoute()
const routerLinkList = [
@ -96,11 +99,10 @@ async function handleLogin() {
async function handleLogout() {
try {
await apiLogout()
await userStore.logout()
} catch (e) {
console.error(e)
}
userStore.logout()
message.success('退出登录成功')
//
isMobileMenuOpen.value = false
@ -150,7 +152,7 @@ function handleMobileLinkClick() {
<!-- 3. 桌面端按钮 (在大屏幕 md 以上显示小屏幕隐藏) -->
<div class="hidden items-center md:flex space-x-4">
<template v-if="!userStore.user">
<template v-if="!userStore.userInfo">
<button
class="rounded-md bg-blue-600 px-4 py-2 text-sm text-white font-medium transition-colors dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-600"
@click="openLoginModal"
@ -174,13 +176,12 @@ function handleMobileLinkClick() {
</a>
</div>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-700 dark:text-gray-200">欢迎, {{ userStore.user.username }}</span>
<span class="text-sm text-gray-700 dark:text-gray-200">欢迎, {{ userStore.userInfo.username }}</span>
<a href="https://github.com/antfu/vitesse" target="_blank" class="text-gray-400 transition-colors hover:text-gray-500 dark:hover:text-gray-300">
<span class="sr-only">GitHub</span>
<div i-carbon:document-horizontal class="text-xl text-gray-8" />
</a>
<button
v-show="false"
class="rounded-md bg-red-600 px-4 py-2 text-sm text-white font-medium transition-colors dark:bg-red-500 hover:bg-red-700 dark:hover:bg-red-600"
@click="handleLogout"
>
@ -243,7 +244,7 @@ function handleMobileLinkClick() {
<!-- 移动端底部的登录/用户信息区域 -->
<div class="border-t border-gray-200 pb-3 pt-4 dark:border-gray-700">
<div class="px-2 space-y-2">
<template v-if="!userStore.user">
<template v-if="!userStore.userInfo">
<button
class="block w-full rounded-md px-3 py-2 text-left text-base text-gray-700 font-medium hover:bg-gray-50 dark:text-gray-200 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-white"
@click="openLoginModal"
@ -267,7 +268,7 @@ function handleMobileLinkClick() {
</a>
</div>
<div class="mb-3 flex items-center px-3">
<span>欢迎, {{ userStore.user.username }}</span>
<span>欢迎, {{ userStore.userInfo.username }}</span>
<button
class="block rounded-md px-3 py-2 text-left text-base text-red-600 font-medium hover:bg-red-50 dark:text-red-400 dark:hover:bg-gray-800"
@click="handleLogout"

View File

@ -321,14 +321,14 @@ const filteredSchools = computed(() => {
<div class="select-none lg:col-span-3">
<div class="rounded-2xl bg-white p-6 text-gray-700 shadow-xl dark:border-gray-700 dark:bg-gray-800 dark:text-white">
<!-- Show different content based on login status -->
<div v-if="!userStore.user">
<div v-if="!userStore.userInfo">
<div class="mb-4 text-center">
<p class="mb-4 text-gray-600">
请先登录开始志愿填报
</p>
<button
class="w-full rounded-full bg-blue-600 px-6 py-3 text-lg text-white font-semibold shadow-lg transition-colors hover:bg-blue-700 hover:shadow-xl"
@click="$router.push('/login')"
@click="userStore.userInfo ? null : $router.push('/login')"
>
登录
</button>

View File

@ -3,9 +3,9 @@ import NProgress from 'nprogress'
import CryptoJS from 'crypto-js'
import { useUserStore } from '~/stores/user'
import message from '~/utils/message'
export interface RequestConfig extends AxiosRequestConfig {
showLoading?: boolean
import { InternalAxiosRequestConfig } from 'axios'
interface CustomRequestConfig extends InternalAxiosRequestConfig {
showLoading?: boolean,
showError?: boolean
}
@ -23,37 +23,35 @@ class Request {
// Request interceptor
this.instance.interceptors.request.use(
(config: RequestConfig) => {
if (config.showLoading !== false) {
(config: InternalAxiosRequestConfig & { showLoading?: boolean }) => {
const customConfig = config as CustomRequestConfig
if (customConfig.showLoading !== false) {
NProgress.start()
}
const userStore = useUserStore()
config.headers = config.headers || {}
customConfig.headers = customConfig.headers || {}
if (userStore.token) {
config.headers.token = userStore.token
customConfig.headers['Authorization'] = 'Bearer ' + userStore.token
}
// Add Signature
const timestamp = Date.now().toString()
const secret = import.meta.env.VITE_API_SECRET || ''
const sign = CryptoJS.MD5(timestamp + secret).toString()
console.log(timestamp)
config.headers['X-App-Sign'] = sign
config.headers['X-App-Timestamp'] = timestamp
return config
customConfig.headers['X-App-Sign'] = sign
customConfig.headers['X-App-Timestamp'] = timestamp
return customConfig
},
(error) => {
return Promise.reject(error)
}
)
)
// Response interceptor
this.instance.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
const config = response.config as RequestConfig
const config = response.config as CustomRequestConfig
if (config.showLoading !== false) {
NProgress.done()
}
@ -69,7 +67,7 @@ class Request {
}
},
(error) => {
const config = error.config as RequestConfig
const config = error.config as CustomRequestConfig
if (config?.showLoading !== false) {
NProgress.done()
}
@ -109,23 +107,23 @@ class Request {
)
}
request<T = any>(config: RequestConfig): Promise<T> {
request<T = any>(config: CustomRequestConfig): Promise<T> {
return this.instance.request(config)
}
get<T = any>(url: string, config?: RequestConfig): Promise<T> {
get<T = any>(url: string, config?: CustomRequestConfig): Promise<T> {
return this.instance.get(url, config)
}
post<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
post<T = any>(url: string, data?: any, config?: CustomRequestConfig): Promise<T> {
return this.instance.post(url, data, config)
}
put<T = any>(url: string, data?: any, config?: RequestConfig): Promise<T> {
put<T = any>(url: string, data?: any, config?: CustomRequestConfig): Promise<T> {
return this.instance.put(url, data, config)
}
delete<T = any>(url: string, config?: RequestConfig): Promise<T> {
delete<T = any>(url: string, config?: CustomRequestConfig): Promise<T> {
return this.instance.delete(url, config)
}
}

View File

@ -10,7 +10,7 @@ export interface UserInfo {
email: string
}
import { login as apiLogin, type LoginParams } from '~/service/api/auth'
import { login as apiLogin, logout as apiLogout, type LoginParams } from '~/service/api/auth'
export const useUserStore = defineStore('user', () => {
const token = ref<string>('')
@ -37,6 +37,16 @@ export const useUserStore = defineStore('user', () => {
localStorage.setItem('token', newToken)
}
function clearToken() {
token.value = ''
localStorage.removeItem('token')
}
function clearUserInfo() {
userInfo.value = null
localStorage.removeItem('user')
}
function setUserInfo(info: UserInfo) {
userInfo.value = info
localStorage.setItem('user', JSON.stringify(info))
@ -53,11 +63,17 @@ export const useUserStore = defineStore('user', () => {
}
}
function logout() {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
localStorage.removeItem('user')
async function logout() {
if(token.value){
try {
await apiLogout()
} catch (error) {
console.error('Failed to logout', error)
}finally{
clearToken()
clearUserInfo()
}
}
}
return {

View File

@ -1,14 +1,15 @@
# Task Detail
## 2025-12-18 Session 5
## 2025-12-18 Session 6
### Execution Reason
User reported that the frontend was displaying a generic "Request failed with status code 401" error instead of the specific "用户不存在" message returned by the backend in the `data.message` field.
User asked how to update the navigation state after login.
### Execution Process
1. **Analysis**: Examined `src/service/request/index.ts`. The response interceptor's error handler (onRejected) was hardcoding messages for 401, 403, etc., ignoring the `message` field from the backend response body (`error.response.data.message`).
2. **Fix**: Updated the error handling logic to prioritize `error.response.data?.message` if it exists. If not, it falls back to the default status code descriptions.
3. **Refinement**: Ensured `Promise.reject` returns a new `Error` object with the refined message, so calling code (`try/catch`) receives the correct error text.
1. **Investigation**: Checked `src/components/TheNavigation.vue` and `src/pages/index.vue`. Found they were accessing `userStore.user`, but the store (updated in Session 1) actually exposes `userStore.userInfo`.
2. **Fix**: Replaced all occurrences of `userStore.user` with `userStore.userInfo` in both `TheNavigation.vue` and `index.vue`.
3. **Result**: Since Pinia stores are reactive, correctly binding to `userStore.userInfo` ensures that when `userStore.login` updates `userInfo`, the UI (Navigation and Home page) automatically re-renders to show the logged-in state (username, logout button, etc.).
### Execution Result
- The application will now display the specific error message returned by the backend (e.g., "用户不存在") in the global message toast and in `catch` blocks, improving the user experience and debugging capability.
- Fixed the reactivity issue by correcting the property name from `user` to `userInfo`.
- Navigation bar will now correctly toggle between "Login" and "User Profile" upon successful login.