This commit is contained in:
Kunagisa 2025-06-02 21:06:39 +08:00
parent 7d77a11dee
commit 86c7e52551
9 changed files with 380 additions and 228 deletions

53
src/api/axiosConfig.js Normal file
View File

@ -0,0 +1,53 @@
import axios from 'axios';
import { logoutUser } from '../utils/jwt'; // logoutUser会处理清除存储和重定向
const API_BASE_URL = 'http://zybdatasupport.online:8000';
const axiosInstance = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
timeout: 10000
});
// 请求拦截器
axiosInstance.interceptors.request.use(
config => {
const token = localStorage.getItem('access_token');
const url = config.url;
// 定义不需要Token的接口条件
const noAuthRequired =
url === '/user/login' ||
url === '/user/register' || // 明确添加注册接口
url.endsWith('/getlist');
if (token && !noAuthRequired) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器
axiosInstance.interceptors.response.use(
response => response,
error => {
const originalRequest = error.config;
// 如果收到401错误并且不是来自登录请求本身
if (error.response && error.response.status === 401 && originalRequest.url !== '/user/login') {
logoutUser(); // 调用简化的logoutUser它只清除token不导航
}
// 不需要额外的console.error错误会自然地在调用处被捕获或显示在网络请求中
return Promise.reject(error);
}
);
export default axiosInstance;

View File

@ -1,50 +1,4 @@
import axios from 'axios';
const API_BASE_URL = 'http://zybdatasupport.online:8000'; // 与 tournament.js 一致
// 创建 axios 实例,可以复用 tournament.js 中的拦截器逻辑,或者单独设置
const axiosInstance = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
timeout: 10000
});
// 请求拦截器 (添加token)
axiosInstance.interceptors.request.use(
config => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `bearer ${token}`;
}
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器 (基本错误处理)
axiosInstance.interceptors.response.use(
response => response,
error => {
if (error.response) {
console.error('API请求错误 (demands.js):', {
status: error.response.status,
data: error.response.data,
config: error.config
});
} else if (error.request) {
console.error('网络错误 (demands.js):', error.request);
} else {
console.error('请求配置错误 (demands.js):', error.message);
}
return Promise.reject(error);
}
);
import axiosInstance from './axiosConfig';
/**
* 获取需求列表
@ -94,18 +48,9 @@ export const addDemand = async (demandData) => {
*/
export const updateDemand = async (id, dataToUpdate) => {
try {
// 根据后端PUT /demands/update/{id} 的定义它期望整个DemandModel作为item
// 但只更新了 sendcontent。为安全起见先获取原始数据再更新。
// 或者如果API设计为只接受要修改的字段则直接发送 { sendcontent: dataToUpdate.sendcontent }
// 这里假设API会处理部分更新或者前端会发送完整的模型即使只改一个字段
// 为简单起见我们先假设API能接受只包含sendcontent的更新或者前端需要先获取完整模型再提交
// 参照后端 item: DemandModel, 它会接收一个完整的模型,但仅使用了 item.sendcontent
// 因此,我们需要传递一个至少包含 sendcontent 的对象,但为了模型验证,最好是完整的模型结构
const payload = {
sendcontent: dataToUpdate.sendcontent,
// 根据 DemandModel补齐其他必填或可选字段即使它们不被后端 update 逻辑使用
// 这部分需要参照 DemandModel 的具体定义来决定哪些字段是必要的
// 以下为推测,需要根据实际 DemandModel 调整
requester: dataToUpdate.requester || '',
qq_code: dataToUpdate.qq_code || '',
content: dataToUpdate.sendcontent, // 保持一致

View File

@ -1,80 +1,75 @@
import axios from 'axios'
import axiosInstance from './axiosConfig';
import { loginSuccess } from '../utils/jwt';
const API_BASE_URL = 'http://zybdatasupport.online:8000'
// const API_BASE_URL = 'http://zybdatasupport.online:8000' // 不再需要
// 创建 axios 实例
const axiosInstance = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
timeout: 10000 // 添加超时设置
})
// // 创建 axios 实例 // 不再需要
// const axiosInstance = axios.create({
// baseURL: API_BASE_URL,
// headers: {
// 'Content-Type': 'application/json',
// 'Accept': 'application/json',
// 'X-Requested-With': 'XMLHttpRequest'
// },
// timeout: 10000 // 添加超时设置
// })
// 设置请求拦截器,自动添加 token
axiosInstance.interceptors.request.use(
config => {
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `bearer ${token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
// // 设置请求拦截器,自动添加 token // 不再需要
// axiosInstance.interceptors.request.use(
// config => {
// const token = localStorage.getItem('access_token')
// if (token) {
// config.headers.Authorization = `bearer ${token}`
// }
// return config
// },
// error => {
// return Promise.reject(error)
// }
// )
// 添加响应拦截器
axiosInstance.interceptors.response.use(
response => response,
error => {
if (error.response) {
// 服务器返回错误状态码
console.error('请求错误:', {
status: error.response.status,
data: error.response.data,
config: error.config
})
} else if (error.request) {
// 请求已发出但没有收到响应
console.error('网络错误:', error.request)
} else {
// 请求配置出错
console.error('请求配置错误:', error.message)
}
return Promise.reject(error)
}
)
// // 添加响应拦截器 // 不再需要
// axiosInstance.interceptors.response.use(
// response => response,
// error => {
// if (error.response) {
// // 服务器返回错误状态码
// console.error('请求错误:', {
// status: error.response.status,
// data: error.response.data,
// config: error.config
// })
// } else if (error.request) {
// // 请求已发出但没有收到响应
// console.error('网络错误:', error.request)
// } else {
// // 请求配置出错
// console.error('请求配置错误:', error.message)
// }
// return Promise.reject(error)
// }
// )
export const userLogin = async (username, password, server, token) => {
try {
console.log('登录请求参数:', { username, password, server, token })
// console.log('登录请求参数:', { username, password, server, token }); // 保留此调试日志以备将来使用,或按需移除
const response = await axiosInstance.post('/user/login', {
username,
password,
server,
token
})
});
// 保存 token 到 localStorage
if (response.data.access_token) {
localStorage.setItem('access_token', response.data.access_token)
loginSuccess(response.data.access_token, username); // 使用 username 作为 userId
}
return response.data
return response.data;
} catch (error) {
console.error('登录失败:', {
status: error.response?.status,
data: error.response?.data,
message: error.message,
config: error.config
})
throw error
// 错误将由响应拦截器统一处理和记录,这里可以直接抛出
throw error;
}
}
};
export const userRegister = async (qq_code, password, server, token) => {
try {
@ -83,20 +78,14 @@ export const userRegister = async (qq_code, password, server, token) => {
password,
server,
token
}
console.log('注册请求URL:', `${API_BASE_URL}/user/register/`)
};
// console.log('注册请求参数:', requestData); // 保留此调试日志以备将来使用,或按需移除
const response = await axiosInstance.post('/user/register', requestData)
console.log('注册响应数据:', response.data)
return response.data
const response = await axiosInstance.post('/user/register', requestData);
// console.log('注册响应数据:', response.data); // 保留此调试日志以备将来使用,或按需移除
return response.data;
} catch (error) {
console.error('注册请求失败:', {
status: error.response?.status,
data: error.response?.data,
message: error.message,
url: error.config?.url
})
throw error
throw error;
}
}
};

View File

@ -1,50 +1,50 @@
import axios from 'axios'
import axiosInstance from './axiosConfig';
const API_BASE_URL = 'http://zybdatasupport.online:8000'
// const API_BASE_URL = 'http://zybdatasupport.online:8000' // 不再需要
// 创建 axios 实例
const axiosInstance = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
timeout: 10000
})
// // 创建 axios 实例 // 不再需要
// const axiosInstance = axios.create({
// baseURL: API_BASE_URL,
// headers: {
// 'Content-Type': 'application/json',
// 'Accept': 'application/json',
// 'X-Requested-With': 'XMLHttpRequest'
// },
// timeout: 10000
// })
// 设置请求拦截器,自动添加 token
axiosInstance.interceptors.request.use(
config => {
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `bearer ${token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
// // 设置请求拦截器,自动添加 token // 不再需要
// axiosInstance.interceptors.request.use(
// config => {
// const token = localStorage.getItem('access_token')
// if (token) {
// config.headers.Authorization = `Bearer ${token}`
// }
// return config
// },
// error => {
// return Promise.reject(error)
// }
// )
// 添加响应拦截器
axiosInstance.interceptors.response.use(
response => response,
error => {
if (error.response) {
console.error('请求错误:', {
status: error.response.status,
data: error.response.data,
config: error.config
})
} else if (error.request) {
console.error('网络错误:', error.request)
} else {
console.error('请求配置错误:', error.message)
}
return Promise.reject(error)
}
)
// // 添加响应拦截器 // 不再需要
// axiosInstance.interceptors.response.use(
// response => response,
// error => {
// if (error.response) {
// console.error('请求错误:', {
// status: error.response.status,
// data: error.response.data,
// config: error.config
// })
// } else if (error.request) {
// console.error('网络错误:', error.request)
// } else {
// console.error('请求配置错误:', error.message)
// }
// return Promise.reject(error)
// }
// )
/**
* 添加赛事
@ -175,17 +175,18 @@ export const getSignUpResultList = async () => {
// 更新参赛结果
export const updateSignUpResult = async (id, data) => {
try {
// 更新报名信息
console.log('更新报名信息...')
await axiosInstance.put(`/tournament/signup/update/${id}`, {
tournament_id: parseInt(data.tournament_id),
type: data.team_name ? 'teamname' : 'individual',
teamname: data.team_name || '',
faction: data.faction || 'random',
username: data.sign_name,
qq: data.qq || ''
})
console.log('报名信息更新成功')
// // 更新报名信息 (这部分逻辑根据您的要求被注释掉)
// console.log('更新报名信息...')
// await axiosInstance.put(`/tournament/signup/update/${id}`, {
// tournament_id: parseInt(data.tournament_id),
// type: data.team_name ? 'teamname' : 'individual',
// teamname: data.team_name || '',
// faction: data.faction || 'random',
// username: data.sign_name,
// qq: data.qq || ''
// })
// console.log('报名信息更新成功')
// 更新报名结果
console.log('更新报名结果...')
await axiosInstance.put(`/tournament/signup_result/update/${id}`, {
@ -218,10 +219,10 @@ export const deleteSignUpResult = async (id) => {
await axiosInstance.delete(`/tournament/signup_result/delete/${id}`)
console.log('报名结果删除成功')
// 删除报名信息
console.log('删除报名信息...')
await axiosInstance.delete(`/tournament/signup/delete/${id}`)
console.log('报名信息删除成功')
// // 删除报名信息 (这部分逻辑根据您的要求被注释掉)
// console.log('删除报名信息...')
// await axiosInstance.delete(`/tournament/signup/delete/${id}`)
// console.log('报名信息删除成功')
return { success: true }
} catch (error) {

View File

@ -64,7 +64,7 @@
<script setup>
import { ref, watch, defineProps, defineEmits } from 'vue';
import { addSignUp, updateSignUpResult } from '../../api/tournament';
import { addSignUp, updateSignUpResult, getSignUpResultList } from '../../api/tournament';
const props = defineProps({
visible: Boolean,
@ -130,16 +130,37 @@ const submitForm = async () => {
try {
if (props.isEditMode) {
const { id, ...dataToUpdate } = form.value;
// updateSignUpResult API player id
await updateSignUpResult(id, dataToUpdate);
const { id, ...originalFormData } = form.value;
// APIwinlose
const dataForApi = {
...originalFormData,
win: String(originalFormData.win || '0'),
lose: String(originalFormData.lose || '0')
};
try {
const response = await getSignUpResultList();
const existingRecord = response.find(item => item.id === id);
if (!existingRecord) {
throw new Error('找不到要更新的报名记录');
}
// 使API
await updateSignUpResult(id, dataForApi);
alert('参赛记录更新成功!');
} catch (error) {
console.error('更新参赛记录失败:', error);
errorMessage.value = error.response?.data?.detail || error.message || '更新失败,请重试';
return;
}
} else {
// addSignUp API tournament_id as id, tournament_name
//
const dataToAdd = {
...form.value,
id: form.value.tournament_id, // tournament_id addSignUp id
type: form.value.team_name ? 'teamname' : 'individual'
id: form.value.tournament_id,
type: form.value.team_name ? 'teamname' : 'individual',
// : form.value.win form.value.lose initialFormState '0'
// addSignUp API String()
};
await addSignUp(dataToAdd);
alert('参赛记录添加成功!');

View File

@ -1,4 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router'
import { hasValidToken, getUserInfo, logoutUser } from '../utils/jwt';
import { justLoggedIn } from '../utils/authSessionState';
const routes = [
{
@ -97,21 +99,43 @@ const router = createRouter({
})
// 路由守卫
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('access_token')
router.beforeEach(async (to, from, next) => {
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!token) {
if (requiresAuth) {
const tokenExists = hasValidToken();
if (justLoggedIn.value) {
justLoggedIn.value = false;
if (tokenExists) {
next();
} else {
logoutUser();
next({ path: '/backend/login', query: { redirect: to.fullPath, sessionExpired: 'true' }});
}
return;
}
if (tokenExists) {
const user = await getUserInfo();
if (user) {
next();
} else {
logoutUser();
next({
path: '/backend/login',
query: { redirect: to.fullPath, sessionExpired: 'true' }
});
}
} else {
next({
path: '/backend/login',
query: { redirect: to.fullPath }
})
} else {
next()
});
}
} else {
next()
next();
}
})
});
export default router

View File

@ -0,0 +1,7 @@
import { ref } from 'vue';
// This flag is used by the router guard to know if a navigation
// is occurring immediately after a successful login.
// If true, the guard can choose to bypass an immediate session check (e.g., getUserInfo)
// for that first navigation, assuming the new token is valid.
export const justLoggedIn = ref(false);

70
src/utils/jwt.js Normal file
View File

@ -0,0 +1,70 @@
import axiosInstance from '../api/axiosConfig';
import router from '../router'; // 引入 Vue Router 实例
import { justLoggedIn } from './authSessionState'; // Import the flag
const USER_INFO_URL = '/user'; // 获取用户信息的API端点
/**
* 检查Token是否存在且理论上是否在有效期内
* 服务端 /user接口会验证实际有效期
* @returns {boolean} 如果存在token则返回true否则false
*/
export const hasValidToken = () => {
const token = localStorage.getItem('access_token');
// 简单的存在性检查,实际有效期由 /user 接口判断
return !!token;
};
/**
* 获取当前用户信息
* 如果Token无效或过期API会返回401响应拦截器将处理清除token
* @returns {Promise<Object|null>} 用户信息对象或null如果未登录或获取失败
*/
export const getUserInfo = async () => {
if (!hasValidToken()) {
// console.log('jwt.js: No token found, skipping getUserInfo.');
return null;
}
try {
// console.log('jwt.js: Attempting to fetch user info from', USER_INFO_URL);
const response = await axiosInstance.get(USER_INFO_URL);
// console.log('jwt.js: User info received:', response.data);
return response.data; // 假设API成功时返回用户信息对象
} catch (error) {
// console.error('jwt.js: Error fetching user info:', error.response ? error.response.status : error.message);
// 401错误会被响应拦截器处理清除token然后错误会传播到这里
// 其他网络错误等也会被捕获
return null;
}
};
/**
* 处理用户登出
* 仅清除本地存储的认证信息
*/
export const logoutUser = () => { // 不再是 async因为它不执行异步导航
// console.log('jwt.js: logoutUser called. Clearing local storage.');
localStorage.removeItem('access_token');
localStorage.removeItem('user_id'); // 如果您也存储了user_id
// 导航将由调用者(如路由守卫)处理
};
/**
* 登录成功后调用存储token和用户信息并处理重定向
* @param {string} accessToken - 从登录API获取的访问令牌
* @param {string} userId - 用户ID
*/
export const loginSuccess = (accessToken, userId) => {
// console.log('jwt.js: loginSuccess. Storing token and user ID.');
localStorage.setItem('access_token', accessToken);
if (userId) {
localStorage.setItem('user_id', userId);
}
justLoggedIn.value = true; // Set the flag before navigation
// 登录成功后重定向
// 尝试获取之前尝试访问的路径,否则重定向到默认路径(例如仪表盘)
const redirectPath = router.currentRoute.value.query.redirect || '/backend/dashboard';
router.replace(redirectPath);
};

View File

@ -38,13 +38,21 @@
</div>
</div>
<div v-else class="dashboard-content non-admin-view">
<div class="non-admin-card">
<div class="welcome-message">欢迎回来</div>
<div class="info-text">
您当前没有管理员权限请选择以下操作
</div>
<div class="button-group">
<button @click="handleLogout" class="logout-button">
退出登录
</button>
<button @click="goToHomePage" class="home-button">
<!-- Optional: Add an icon here -->
返回主页面
</button>
<button @click="handleLogout" class="logout-button">
<!-- Optional: Add an icon here -->
退出登录
</button>
</div>
</div>
</div>
</div>
@ -236,37 +244,71 @@ html, body, .dashboard-wrapper, .admin-layout {
transition: margin-left 0.3s ease;
}
/* Non-Admin View (Centered content, similar to before but within new wrapper) */
/* Non-Admin View Enhancements */
.dashboard-content.non-admin-view {
display: flex; /* Use flex to center content */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%; /* Take full height of the wrapper */
min-height: 100vh; /* Use min-height to ensure it covers viewport even if content is small */
width: 100%;
padding: 20px;
/* background-color: #fff; Already white from admin-main-content, but can be explicit if needed */
padding: 30px;
background-color: #f0f2f5; /* Consistent light background */
text-align: center; /* Center text for any messages */
}
.non-admin-view .button-group {
background-color: #fff;
padding: 30px 40px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
text-align: center;
max-width: 400px;
.non-admin-view .non-admin-card {
background-color: #ffffff;
padding: 40px 50px;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
max-width: 450px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center; /* Center buttons if they are directly in this card */
}
.non-admin-view .welcome-message {
font-size: 1.5rem; /* Slightly larger welcome/info text */
color: #333;
margin-bottom: 15px;
font-weight: 500;
}
.non-admin-view .info-text {
font-size: 0.95rem;
color: #666;
margin-bottom: 30px;
line-height: 1.6;
}
.non-admin-view .button-group {
/* Removed background, padding, shadow from here as it's now on non-admin-card */
width: 100%; /* Button group takes full width of the card */
display: flex;
flex-direction: column; /* Stack buttons vertically */
gap: 15px; /* Space between buttons */
}
.non-admin-view .button-group button {
padding: 10px 20px;
border-radius: 6px;
padding: 12px 25px; /* Slightly larger padding */
border-radius: 8px; /* Slightly more rounded */
font-weight: 500;
transition: background-color 0.2s;
font-size: 1rem; /* Standardized font size */
transition: background-color 0.2s, transform 0.1s;
border: none;
color: #fff;
cursor: pointer;
margin: 8px; /* Provides spacing around buttons, e.g., 8px top/bottom, 8px left/right */
min-width: 120px; /* Ensure buttons have a decent minimum width */
width: 100%; /* Make buttons take full width of their container */
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.non-admin-view .button-group button:hover {
transform: translateY(-2px); /* Subtle lift on hover */
}
.non-admin-view .button-group .home-button {