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.sourceLanguage": "zh-CN",
|
||||||
"i18n-ally.keystyle": "nested",
|
"i18n-ally.keystyle": "nested",
|
||||||
"i18n-ally.localesPaths": "locales",
|
"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>
|
</p>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<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>
|
<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>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ const isMobileMenuOpen = ref(false)
|
||||||
const username = ref('')
|
const username = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
// 获取路由实例
|
||||||
|
const router = useRouter()
|
||||||
|
// 获取当前路由信息
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const routerLinkList = [
|
const routerLinkList = [
|
||||||
|
|
@ -96,11 +99,10 @@ async function handleLogin() {
|
||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
try {
|
try {
|
||||||
await apiLogout()
|
await userStore.logout()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
userStore.logout()
|
|
||||||
message.success('退出登录成功')
|
message.success('退出登录成功')
|
||||||
// 登出后关闭菜单
|
// 登出后关闭菜单
|
||||||
isMobileMenuOpen.value = false
|
isMobileMenuOpen.value = false
|
||||||
|
|
@ -150,7 +152,7 @@ function handleMobileLinkClick() {
|
||||||
|
|
||||||
<!-- 3. 桌面端按钮 (在大屏幕 md 以上显示,小屏幕隐藏) -->
|
<!-- 3. 桌面端按钮 (在大屏幕 md 以上显示,小屏幕隐藏) -->
|
||||||
<div class="hidden items-center md:flex space-x-4">
|
<div class="hidden items-center md:flex space-x-4">
|
||||||
<template v-if="!userStore.user">
|
<template v-if="!userStore.userInfo">
|
||||||
<button
|
<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"
|
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"
|
@click="openLoginModal"
|
||||||
|
|
@ -174,13 +176,12 @@ function handleMobileLinkClick() {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-2">
|
<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">
|
<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>
|
<span class="sr-only">GitHub</span>
|
||||||
<div i-carbon:document-horizontal class="text-xl text-gray-8" />
|
<div i-carbon:document-horizontal class="text-xl text-gray-8" />
|
||||||
</a>
|
</a>
|
||||||
<button
|
<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"
|
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"
|
@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="border-t border-gray-200 pb-3 pt-4 dark:border-gray-700">
|
||||||
<div class="px-2 space-y-2">
|
<div class="px-2 space-y-2">
|
||||||
<template v-if="!userStore.user">
|
<template v-if="!userStore.userInfo">
|
||||||
<button
|
<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"
|
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"
|
@click="openLoginModal"
|
||||||
|
|
@ -267,7 +268,7 @@ function handleMobileLinkClick() {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3 flex items-center px-3">
|
<div class="mb-3 flex items-center px-3">
|
||||||
<span>欢迎, {{ userStore.user.username }}</span>
|
<span>欢迎, {{ userStore.userInfo.username }}</span>
|
||||||
<button
|
<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"
|
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"
|
@click="handleLogout"
|
||||||
|
|
|
||||||
|
|
@ -321,14 +321,14 @@ const filteredSchools = computed(() => {
|
||||||
<div class="select-none lg:col-span-3">
|
<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">
|
<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 -->
|
<!-- Show different content based on login status -->
|
||||||
<div v-if="!userStore.user">
|
<div v-if="!userStore.userInfo">
|
||||||
<div class="mb-4 text-center">
|
<div class="mb-4 text-center">
|
||||||
<p class="mb-4 text-gray-600">
|
<p class="mb-4 text-gray-600">
|
||||||
请先登录,开始志愿填报
|
请先登录,开始志愿填报
|
||||||
</p>
|
</p>
|
||||||
<button
|
<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"
|
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>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ import NProgress from 'nprogress'
|
||||||
import CryptoJS from 'crypto-js'
|
import CryptoJS from 'crypto-js'
|
||||||
import { useUserStore } from '~/stores/user'
|
import { useUserStore } from '~/stores/user'
|
||||||
import message from '~/utils/message'
|
import message from '~/utils/message'
|
||||||
|
import { InternalAxiosRequestConfig } from 'axios'
|
||||||
export interface RequestConfig extends AxiosRequestConfig {
|
interface CustomRequestConfig extends InternalAxiosRequestConfig {
|
||||||
showLoading?: boolean
|
showLoading?: boolean,
|
||||||
showError?: boolean
|
showError?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,37 +23,35 @@ class Request {
|
||||||
|
|
||||||
// Request interceptor
|
// Request interceptor
|
||||||
this.instance.interceptors.request.use(
|
this.instance.interceptors.request.use(
|
||||||
(config: RequestConfig) => {
|
(config: InternalAxiosRequestConfig & { showLoading?: boolean }) => {
|
||||||
if (config.showLoading !== false) {
|
const customConfig = config as CustomRequestConfig
|
||||||
|
|
||||||
|
if (customConfig.showLoading !== false) {
|
||||||
NProgress.start()
|
NProgress.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
config.headers = config.headers || {}
|
customConfig.headers = customConfig.headers || {}
|
||||||
|
|
||||||
if (userStore.token) {
|
if (userStore.token) {
|
||||||
config.headers.token = userStore.token
|
customConfig.headers['Authorization'] = 'Bearer ' + userStore.token
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Signature
|
// Add Signature
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
const secret = import.meta.env.VITE_API_SECRET || ''
|
const secret = import.meta.env.VITE_API_SECRET || ''
|
||||||
const sign = CryptoJS.MD5(timestamp + secret).toString()
|
const sign = CryptoJS.MD5(timestamp + secret).toString()
|
||||||
console.log(timestamp)
|
customConfig.headers['X-App-Sign'] = sign
|
||||||
config.headers['X-App-Sign'] = sign
|
customConfig.headers['X-App-Timestamp'] = timestamp
|
||||||
config.headers['X-App-Timestamp'] = timestamp
|
return customConfig
|
||||||
|
|
||||||
return config
|
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Response interceptor
|
// Response interceptor
|
||||||
this.instance.interceptors.response.use(
|
this.instance.interceptors.response.use(
|
||||||
(response: AxiosResponse<ApiResponse>) => {
|
(response: AxiosResponse<ApiResponse>) => {
|
||||||
const config = response.config as RequestConfig
|
const config = response.config as CustomRequestConfig
|
||||||
if (config.showLoading !== false) {
|
if (config.showLoading !== false) {
|
||||||
NProgress.done()
|
NProgress.done()
|
||||||
}
|
}
|
||||||
|
|
@ -69,7 +67,7 @@ class Request {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
const config = error.config as RequestConfig
|
const config = error.config as CustomRequestConfig
|
||||||
if (config?.showLoading !== false) {
|
if (config?.showLoading !== false) {
|
||||||
NProgress.done()
|
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)
|
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)
|
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)
|
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)
|
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)
|
return this.instance.delete(url, config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export interface UserInfo {
|
||||||
email: string
|
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', () => {
|
export const useUserStore = defineStore('user', () => {
|
||||||
const token = ref<string>('')
|
const token = ref<string>('')
|
||||||
|
|
@ -37,6 +37,16 @@ export const useUserStore = defineStore('user', () => {
|
||||||
localStorage.setItem('token', newToken)
|
localStorage.setItem('token', newToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearToken() {
|
||||||
|
token.value = ''
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearUserInfo() {
|
||||||
|
userInfo.value = null
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
}
|
||||||
|
|
||||||
function setUserInfo(info: UserInfo) {
|
function setUserInfo(info: UserInfo) {
|
||||||
userInfo.value = info
|
userInfo.value = info
|
||||||
localStorage.setItem('user', JSON.stringify(info))
|
localStorage.setItem('user', JSON.stringify(info))
|
||||||
|
|
@ -53,11 +63,17 @@ export const useUserStore = defineStore('user', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
async function logout() {
|
||||||
token.value = ''
|
if(token.value){
|
||||||
userInfo.value = null
|
try {
|
||||||
localStorage.removeItem('token')
|
await apiLogout()
|
||||||
localStorage.removeItem('user')
|
} catch (error) {
|
||||||
|
console.error('Failed to logout', error)
|
||||||
|
}finally{
|
||||||
|
clearToken()
|
||||||
|
clearUserInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
# Task Detail
|
# Task Detail
|
||||||
|
|
||||||
## 2025-12-18 Session 5
|
## 2025-12-18 Session 6
|
||||||
|
|
||||||
### Execution Reason
|
### 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
|
### 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`).
|
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**: 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.
|
2. **Fix**: Replaced all occurrences of `userStore.user` with `userStore.userInfo` in both `TheNavigation.vue` and `index.vue`.
|
||||||
3. **Refinement**: Ensured `Promise.reject` returns a new `Error` object with the refined message, so calling code (`try/catch`) receives the correct error text.
|
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
|
### 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