feat:登录、退出
This commit is contained in:
parent
5bae1f8c12
commit
6c16860fae
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,27 +23,25 @@ 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)
|
||||
|
|
@ -53,7 +51,7 @@ class Request {
|
|||
// 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue