活动相关页面详情

This commit is contained in:
username 2025-07-30 02:46:20 +08:00
parent ab31374e1a
commit d8cce4cb46
10 changed files with 1863 additions and 43 deletions

228
src/api/modules/activity.ts Normal file
View File

@ -0,0 +1,228 @@
import request from '../request'
import type { ApiResponse } from '../types'
// 活动相关的类型定义
export interface Activity {
id: number
title: string
subtitle: string
description: string
startDate: string
endDate: string
status: 'upcoming' | 'ongoing' | 'ended'
registrationCount: number
likeCount: number
categories: string[]
timeline: ActivityTimelineItem[]
content: ActivityContentItem[]
images: string[]
organizer: string
coOrganizer?: string
}
export interface ActivityTimelineItem {
date: string
title: string
description: string
}
export interface ActivityContentItem {
text: string
downloadLink?: string
}
export interface ActivityListItem {
id: number
title: string
subtitle: string
courseTitle: string
schedule: string
duration: string
students: string
price: string
status: string
image: string
}
// 活动API类
export class ActivityApi {
// 获取活动列表
static async getActivities(): Promise<ApiResponse<ActivityListItem[]>> {
try {
// 模拟API调用 - 实际项目中这里应该调用真实的API
const mockData: ActivityListItem[] = [
{
id: 1,
title: '计算机二级',
subtitle: 'C语言讲练综合班',
courseTitle: '计算机二级C语言程序设计证书',
schedule: '开课时间2025.07.26-2025.09.28',
duration: '适合年级:高校本科生',
students: '已报名1468/2000',
price: '免费',
status: '进行中',
image: '/images/activity/活动图1.png'
},
{
id: 2,
title: 'AI创新实践',
subtitle: '全国青少年人工智能创新实践活动',
courseTitle: '与AI共创未来 - 2025年全国青少年人工智能创新实践活动',
schedule: '开课时间2025.01.26-2025.09.22',
duration: '适合年级:中小学生',
students: '已报名541/1000',
price: '免费',
status: '报名中',
image: '/images/activity/活动图2.png'
}
]
return {
code: 0,
message: '获取活动列表成功',
data: mockData
}
} catch (error) {
console.error('获取活动列表失败:', error)
return {
code: -1,
message: '获取活动列表失败',
data: []
}
}
}
// 根据ID获取活动详情
static async getActivityById(id: number): Promise<ApiResponse<Activity>> {
try {
// 模拟API调用 - 实际项目中这里应该调用真实的API
const mockActivity: Activity = {
id: id,
title: '"与AI共创未来"',
subtitle: '2025年全国青少年人工智能创新实践活动',
description: '本活动是面向全国以青少年为主体的人工智能DeepSeek该活动以公司自主研发的创新成果以实践工作作为引导向青少年群体宣传推广人工智能技术让青少年了解人工智能技术的发展历程体验人工智能技术带来的便利培养青少年对人工智能技术的兴趣提升青少年的科学素养为我国人工智能技术的发展培养后备人才。在此基础上通过实践活动的开展让青少年在实践中学习在学习中实践。',
startDate: '2025-01-26',
endDate: '2025-09-22',
status: 'ongoing',
registrationCount: 541,
likeCount: 2377,
categories: [
'AI少年明日程',
'AI实习程序员',
'AI工程实践营',
'AI智能未来营',
'AI编程教学创新实验'
],
timeline: [
{
date: '7月1日-7月24日',
title: '青少年编程技能实践活动报名参赛',
description: ''
},
{
date: '7月25日-8月31日',
title: '青少年编程技能竞赛活动、开发实践活动',
description: ''
},
{
date: '8月31日-9月5日',
title: '实践作品提交和技能竞赛活动开展',
description: ''
},
{
date: '9月6日-9月20日',
title: '优秀作品公示期',
description: ''
},
{
date: '9月21日-9月27日',
title: '人工智能实践活动总结活动举办',
description: ''
}
],
content: [
{
text: '中国科协青少年科技中心、中国青少年科技教育工作者协会等',
downloadLink: undefined
},
{
text: '作品提交规则说明',
downloadLink: '#'
},
{
text: '中小学教师人工智能素养提升在线学习平台(网址)',
downloadLink: '#'
},
{
text: '作品提交 报名表',
downloadLink: '#'
}
],
images: [
'/images/activity/活动图1.png',
'/images/activity/活动图2.png',
'/images/activity/活动图3.png'
],
organizer: '中国科协青少年科技中心',
coOrganizer: '中国青少年科技教育工作者协会、上海人工智能实验室'
}
return {
code: 0,
message: '获取活动详情成功',
data: mockActivity
}
} catch (error) {
console.error('获取活动详情失败:', error)
return {
code: -1,
message: '获取活动详情失败',
data: {} as Activity
}
}
}
// 报名活动
static async registerActivity(activityId: number): Promise<ApiResponse<any>> {
try {
// 模拟API调用
console.log('报名活动:', activityId)
return {
code: 0,
message: '报名成功',
data: { success: true }
}
} catch (error) {
console.error('报名活动失败:', error)
return {
code: -1,
message: '报名失败',
data: { success: false }
}
}
}
// 提交作品
static async submitWork(activityId: number, workData: any): Promise<ApiResponse<any>> {
try {
// 模拟API调用
console.log('提交作品:', activityId, workData)
return {
code: 0,
message: '作品提交成功',
data: { success: true }
}
} catch (error) {
console.error('提交作品失败:', error)
return {
code: -1,
message: '作品提交失败',
data: { success: false }
}
}
}
}
export default ActivityApi

View File

@ -17,34 +17,167 @@ export class AuthApi {
// 用户登录
static async login(data: LoginRequest): Promise<ApiResponse<LoginResponse>> {
try {
// 调用后端API
const response = await ApiRequest.post<BackendLoginResponse>('/users/login', data)
console.log('🚀 发送登录请求:', { url: '/users/login', data: { ...data, password: '***' } })
// 适配后端响应格式为前端期望的格式
const adaptedResponse: ApiResponse<LoginResponse> = {
code: response.code,
message: response.message,
data: {
user: {
id: response.data.id, // 使用后端返回的用户ID
email: data.email || '',
phone: data.phone || '',
username: data.phone || data.email?.split('@')[0] || 'user',
nickname: '用户',
avatar: '',
role: 'student',
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
},
token: response.data.token,
refreshToken: '', // 后端没有返回,使用空字符串
expiresIn: 3600 // 默认1小时可以根据expires字段计算
// 调用后端API
const response = await ApiRequest.post<any>('/users/login', data)
console.log('🔍 Login API Response:', response)
console.log('🔍 Response Code:', response.code)
console.log('🔍 Response Data:', response.data)
console.log('🔍 Response Data Type:', typeof response.data)
// 处理响应格式问题 - 如果code和message是undefined可能是响应拦截器的问题
let actualCode = response.code
let actualMessage = response.message
let actualData = response.data
// 如果response.code是undefined检查response.data是否包含完整的API响应
if (actualCode === undefined && actualData && typeof actualData === 'object') {
if ('code' in actualData && 'message' in actualData && 'data' in actualData) {
// 这种情况下真正的API响应被包装在了response.data中
actualCode = actualData.code
actualMessage = actualData.message
actualData = actualData.data
console.log('🔧 修正后的响应:', { code: actualCode, message: actualMessage, data: actualData })
}
}
return adaptedResponse
// 检查响应格式并适配
if (actualCode === 200 || actualCode === 0) {
// 如果后端返回的是完整的用户信息格式mock数据格式
if (actualData && actualData.user && actualData.token) {
return {
code: actualCode,
message: actualMessage,
data: actualData
} as ApiResponse<LoginResponse>
}
// 如果后端返回的是真实API格式包含token, timestamp, expires
if (actualData && actualData.token && actualData.timestamp) {
const adaptedResponse: ApiResponse<LoginResponse> = {
code: actualCode,
message: actualMessage || '登录成功',
data: {
user: {
id: 1, // 真实API没有返回用户ID使用默认值
email: data.email || '',
phone: data.phone || '',
username: data.phone || data.email?.split('@')[0] || 'user',
nickname: '用户',
avatar: '',
role: 'student',
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
},
token: actualData.token,
refreshToken: '', // 真实API没有返回refreshToken
expiresIn: 3600 // 默认1小时过期
}
}
return adaptedResponse
}
// 如果后端返回的是简化格式BackendLoginResponse
if (actualData && actualData.token) {
const adaptedResponse: ApiResponse<LoginResponse> = {
code: actualCode,
message: actualMessage || '登录成功',
data: {
user: {
id: actualData.id || 1,
email: data.email || '',
phone: data.phone || '',
username: data.phone || data.email?.split('@')[0] || 'user',
nickname: '用户',
avatar: '',
role: 'student',
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
},
token: actualData.token,
refreshToken: actualData.refreshToken || '',
expiresIn: actualData.expiresIn || 3600
}
}
return adaptedResponse
}
// 如果只有token字段直接使用token
if (typeof actualData === 'string') {
const adaptedResponse: ApiResponse<LoginResponse> = {
code: actualCode,
message: actualMessage || '登录成功',
data: {
user: {
id: 1,
email: data.email || '',
phone: data.phone || '',
username: data.phone || data.email?.split('@')[0] || 'user',
nickname: '用户',
avatar: '',
role: 'student',
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
},
token: actualData,
refreshToken: '',
expiresIn: 3600
}
}
return adaptedResponse
}
// 如果data是null或undefined但响应成功可能是某些API的特殊情况
if (!actualData) {
console.warn('⚠️ API返回成功但data为空可能需要检查API实现')
throw new Error('登录成功但未返回用户信息')
}
// 尝试处理其他可能的响应格式
console.warn('⚠️ 未知的响应格式,尝试通用处理:', actualData)
// 如果data是对象但不包含预期字段尝试创建默认响应
if (typeof actualData === 'object') {
const adaptedResponse: ApiResponse<LoginResponse> = {
code: actualCode,
message: actualMessage || '登录成功',
data: {
user: {
id: actualData.id || actualData.userId || 1,
email: data.email || '',
phone: data.phone || '',
username: data.phone || data.email?.split('@')[0] || 'user',
nickname: actualData.nickname || actualData.name || '用户',
avatar: actualData.avatar || '',
role: actualData.role || 'student',
status: 'active',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
},
token: actualData.accessToken || actualData.access_token || 'temp_token_' + Date.now(),
refreshToken: actualData.refreshToken || actualData.refresh_token || '',
expiresIn: actualData.expiresIn || actualData.expires_in || 3600
}
}
return adaptedResponse
}
}
// 如果响应格式不符合预期,抛出错误
console.error('🚨 Unexpected response format:', {
code: actualCode,
message: actualMessage,
data: actualData,
dataType: typeof actualData
})
throw new Error(actualMessage || '登录响应格式错误')
} catch (error) {
console.error('🚨 Login API Error:', error)
throw error
}
}
@ -56,7 +189,24 @@ export class AuthApi {
// 用户登出
static logout(): Promise<ApiResponse<null>> {
return ApiRequest.post('/auth/logout')
// 尝试多个可能的登出端点
return ApiRequest.post('/auth/logout').catch(async (error) => {
// 如果 /auth/logout 失败,尝试其他可能的端点
if (error.response?.status === 404) {
try {
return await ApiRequest.post('/users/logout')
} catch (secondError) {
// 如果所有端点都失败,返回成功(客户端登出)
console.log('所有登出端点都不存在,执行客户端登出')
return {
code: 200,
message: '登出成功',
data: null
}
}
}
throw error
})
}
// 刷新Token

View File

@ -198,13 +198,22 @@ export class CourseApi {
}
return adaptedResponse
} catch (error) {
} catch (error: any) {
console.error('课程API调用失败:', error)
let errorMessage = '获取课程列表失败'
if (error.code === 'ECONNABORTED') {
errorMessage = '请求超时,请检查网络连接'
} else if (error.message === 'Network Error') {
errorMessage = '网络连接失败,请检查网络设置'
} else if (error.message?.includes('网络')) {
errorMessage = error.message
}
// 返回空数据而不是抛出错误,确保应用不会崩溃
return {
code: 500,
message: '获取课程列表失败',
message: errorMessage,
data: {
list: [],
total: 0,

View File

@ -4,6 +4,14 @@ import { useUserStore } from '@/stores/user'
// import router from '@/router'
import type { ApiResponse } from './types'
// 网络状态检测
const checkNetworkStatus = (): boolean => {
if (typeof navigator !== 'undefined' && 'onLine' in navigator) {
return navigator.onLine
}
return true // 默认认为网络可用
}
// 消息提示函数 - 使用window.alert作为fallback实际项目中应该使用UI库的消息组件
const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'info' = 'info') => {
// 这里可以替换为你使用的UI库的消息组件
@ -19,7 +27,7 @@ const showMessage = (message: string, type: 'success' | 'error' | 'warning' | 'i
// 创建axios实例
const request: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api',
timeout: 10000,
timeout: 30000, // 增加到30秒
headers: {
'Content-Type': 'application/json',
},
@ -57,7 +65,7 @@ request.interceptors.request.use(
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
(response: AxiosResponse<any>) => {
const { data } = response
// 开发环境下打印响应信息
@ -69,20 +77,37 @@ request.interceptors.response.use(
})
}
// 处理不同的响应格式
let normalizedData: ApiResponse
// 如果响应已经是标准格式
if (data && typeof data === 'object' && 'code' in data && 'message' in data) {
normalizedData = data
} else {
// 如果响应不是标准格式,包装成标准格式
normalizedData = {
code: 200,
message: '请求成功',
data: data
}
}
// 检查业务状态码
if (data.code === 200 || data.code === 0) {
if (normalizedData.code === 200 || normalizedData.code === 0) {
// 返回标准化后的响应
response.data = normalizedData
return response
}
// 处理业务错误
const errorMessage = data.message || '请求失败'
const errorMessage = normalizedData.message || '请求失败'
// 不在这里显示错误消息,让组件自己处理
// showMessage(errorMessage, 'error')
// 创建一个包含完整响应信息的错误对象
const error = new Error(errorMessage)
;(error as any).response = {
data: data,
data: normalizedData,
status: 200 // HTTP状态码是200但业务状态码不是成功
}
return Promise.reject(error)
@ -109,6 +134,13 @@ request.interceptors.response.use(
errorMessage = '没有权限访问'
break
case 404:
// 对于登出接口的404错误不显示错误消息
if (error.config?.url?.includes('/logout')) {
console.log('登出接口不存在,这是正常的')
return Promise.resolve({
data: { code: 200, message: '登出成功', data: null }
})
}
errorMessage = '请求的资源不存在'
break
case 422:
@ -249,6 +281,15 @@ const handleMockRequest = async <T = any>(url: string, method: string, data?: an
} as ApiResponse<T>
}
// 用户登出Mock
if (url === '/auth/logout' && method === 'POST') {
return {
code: 200,
message: '登出成功',
data: null
} as ApiResponse<T>
}
// 课程详情Mock
if (url === '/lesson/detail' && method === 'GET') {
// 对于GET请求参数直接在data中data就是params对象
@ -410,10 +451,44 @@ const handleMockRequest = async <T = any>(url: string, method: string, data?: an
} as ApiResponse<T>
}
// 重试机制
const retryRequest = async <T = any>(
requestFn: () => Promise<T>,
maxRetries: number = 2,
delay: number = 1000
): Promise<T> => {
let lastError: any
for (let i = 0; i <= maxRetries; i++) {
try {
// 检查网络状态
if (!checkNetworkStatus()) {
throw new Error('网络连接不可用,请检查网络设置')
}
return await requestFn()
} catch (error: any) {
lastError = error
// 如果是最后一次重试,或者不是网络错误,直接抛出
if (i === maxRetries || (error.code !== 'ECONNABORTED' && error.message !== 'Network Error' && !error.message?.includes('网络'))) {
throw error
}
console.log(`请求失败,${delay}ms后进行第${i + 1}次重试...`)
console.log('错误详情:', error.message)
await new Promise(resolve => setTimeout(resolve, delay))
delay *= 2 // 指数退避
}
}
throw lastError
}
// 请求方法封装
export class ApiRequest {
// GET 请求
static get<T = any>(
static async get<T = any>(
url: string,
params?: any,
config?: AxiosRequestConfig
@ -422,11 +497,12 @@ export class ApiRequest {
if (import.meta.env.VITE_ENABLE_MOCK === 'true') {
return handleMockRequest<T>(url, 'GET', params)
}
return request.get(url, { params, ...config })
return retryRequest(() => request.get(url, { params, ...config }))
}
// POST 请求
static post<T = any>(
static async post<T = any>(
url: string,
data?: any,
config?: AxiosRequestConfig
@ -435,7 +511,8 @@ export class ApiRequest {
if (import.meta.env.VITE_ENABLE_MOCK === 'true') {
return handleMockRequest<T>(url, 'POST', data)
}
return request.post(url, data, config)
return retryRequest(() => request.post(url, data, config))
}
// PUT 请求
@ -516,6 +593,17 @@ export class ApiRequest {
window.URL.revokeObjectURL(downloadUrl)
})
}
// API健康检查
static async healthCheck(): Promise<boolean> {
try {
const response = await request.get('/health', { timeout: 5000 })
return response.status === 200
} catch (error) {
console.warn('API健康检查失败:', error)
return false
}
}
}
export default request

View File

@ -123,6 +123,8 @@ const handleLogin = async () => {
isLoading.value = true
try {
console.log('🚀 开始登录:', { account: loginForm.account, password: '***' })
//
const isPhone = /^[0-9]+$/.test(loginForm.account)
@ -132,6 +134,8 @@ const handleLogin = async () => {
password: loginForm.password
})
console.log('✅ 登录响应:', response)
if (response.code === 200 || response.code === 0) {
const { user, token, refreshToken } = response.data
@ -141,7 +145,7 @@ const handleLogin = async () => {
//
localStorage.setItem('token', token)
localStorage.setItem('refreshToken', refreshToken)
localStorage.setItem('refreshToken', refreshToken || '')
localStorage.setItem('user', JSON.stringify(user))
//
@ -158,11 +162,12 @@ const handleLogin = async () => {
loginForm.password = ''
loginForm.remember = false
} else {
console.error('❌ 登录失败 - 响应码错误:', response)
message.error(response.message || '登录失败')
}
} catch (error: any) {
console.error('登录失败:', error)
console.error('❌ 登录异常:', error)
//
if (error.response?.status === 401) {

View File

@ -0,0 +1,55 @@
import { ref, onMounted, onUnmounted } from 'vue'
export function useNetworkStatus() {
const isOnline = ref(navigator.onLine)
const isSlowConnection = ref(false)
const updateOnlineStatus = () => {
isOnline.value = navigator.onLine
console.log('网络状态变化:', isOnline.value ? '在线' : '离线')
}
const checkConnectionSpeed = async () => {
if (!isOnline.value) return
try {
const startTime = Date.now()
const response = await fetch('/favicon.ico', {
method: 'HEAD',
cache: 'no-cache'
})
const endTime = Date.now()
const duration = endTime - startTime
// 如果请求时间超过3秒认为是慢连接
isSlowConnection.value = duration > 3000
console.log('连接速度检测:', duration + 'ms', isSlowConnection.value ? '慢连接' : '正常')
} catch (error) {
console.warn('连接速度检测失败:', error)
isSlowConnection.value = true
}
}
onMounted(() => {
window.addEventListener('online', updateOnlineStatus)
window.addEventListener('offline', updateOnlineStatus)
// 初始检测连接速度
checkConnectionSpeed()
// 每30秒检测一次连接速度
const speedCheckInterval = setInterval(checkConnectionSpeed, 30000)
onUnmounted(() => {
window.removeEventListener('online', updateOnlineStatus)
window.removeEventListener('offline', updateOnlineStatus)
clearInterval(speedCheckInterval)
})
})
return {
isOnline,
isSlowConnection,
checkConnectionSpeed
}
}

View File

@ -13,6 +13,7 @@ import LearningPaths from '@/views/LearningPaths.vue'
import Faculty from '@/views/Faculty.vue'
import Resources from '@/views/Resources.vue'
import Activities from '@/views/Activities.vue'
import ActivityDetail from '@/views/ActivityDetail.vue'
import TestSections from '@/views/TestSections.vue'
import VideoTest from '@/views/VideoTest.vue'
@ -101,6 +102,14 @@ const routes: RouteRecordRaw[] = [
title: '全部活动'
}
},
{
path: '/activity/:id',
name: 'ActivityDetail',
component: ActivityDetail,
meta: {
title: '活动详情'
}
},
{
path: '/test-sections',
name: 'TestSections',

View File

@ -30,10 +30,21 @@ export const useUserStore = defineStore('user', () => {
const logout = async () => {
try {
// 调用登出API
await AuthApi.logout()
} catch (error) {
console.error('登出API调用失败:', error)
// 尝试调用登出API如果存在的话
// 但不让API失败阻止登出过程
if (token.value) {
try {
await AuthApi.logout()
console.log('服务器端登出成功')
} catch (error: any) {
// 如果是404错误说明后端没有登出接口这是正常的
if (error.response?.status === 404) {
console.log('后端无登出接口,仅执行客户端登出')
} else {
console.warn('登出API调用失败但继续执行客户端登出:', error.message)
}
}
}
} finally {
// 无论API调用是否成功都清除本地数据
user.value = null
@ -42,6 +53,7 @@ export const useUserStore = defineStore('user', () => {
localStorage.removeItem('refreshToken')
localStorage.removeItem('user')
localStorage.removeItem('rememberMe')
console.log('用户已登出')
}
}

View File

@ -96,6 +96,9 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
//
const loading = ref(true)
@ -173,8 +176,8 @@ const activities = ref([
//
const viewDetail = (id: number) => {
console.log('查看活动详情:', id)
//
// router.push(`/activity/${id}`)
//
router.push(`/activity/${id}`)
}
// 使

1261
src/views/ActivityDetail.vue Normal file

File diff suppressed because it is too large Load Diff