diff --git a/api/student.ts b/api/student.ts index f20c8ee..f9b4ec2 100644 --- a/api/student.ts +++ b/api/student.ts @@ -255,6 +255,14 @@ export type StudentGrowth = { records: GrowthPointRecord[] } +export type StudentPointRecordsPage = { + records: GrowthPointRecord[] + total: number + pageIndex: number + pageSize: number + lastId: number | null +} + export type LearningTaskItem = { id: string title: string @@ -333,6 +341,15 @@ export type StudentLearningWarningResult = { items: StudentLearningWarningItem[] } +const learningWarningTypeMap: { [key: number]: { title: string, level: StudentAlertLevel } } = { + 1: { title: '今日未打卡', level: 'high' }, + 2: { title: '仅学无交', level: 'medium' }, + 3: { title: '作业未完成', level: 'high' }, + 4: { title: '攻克失败', level: 'critical' }, + 5: { title: '连续 3 天未学习', level: 'high' }, + 6: { title: '正确率明显下滑', level: 'medium' } +} + // 验证学生二维码请求参数 export type VerifyQRCodeParams = { qrCode: string @@ -433,26 +450,30 @@ export function apiGetStudentHomeMetrics(studentId: string, showLoading: boolean } /** - * 获取指定学生今日学习预警。已触发预警优先,其后展示未触发规则。 + * 获取指定学生今日学习预警。已触发预警优先,其后展示未触发规则,可按数量裁剪。 */ -export function apiGetStudentLearningWarnings(studentId: string, showLoading: boolean = true, limit?: number): Promise | null> { +export function apiGetStudentLearningWarnings(studentId: string, showLoading: boolean = true, limit?: number, onlyTriggered: boolean = false): Promise | null> { return get(`/api/v1/ParentLearningWarning/GetTodayList?userId=${encodeURIComponent(studentId)}`, undefined, { showLoading }).then((res) => { if (!res || !res.data || !Array.isArray(res.data.items)) return null const warningData = res.data const triggeredItems = warningData.items.filter((item: StudentLearningWarningItem) => Boolean(item.isTriggered)) const normalItems = warningData.items.filter((item: StudentLearningWarningItem) => !Boolean(item.isTriggered)) - const sourceItems = triggeredItems.concat(normalItems) + const orderedItems = triggeredItems.concat(normalItems) + const sourceItems = onlyTriggered ? triggeredItems : orderedItems const visibleItems = limit === undefined ? sourceItems : sourceItems.slice(0, limit) - const list: StudentAlert[] = visibleItems.map((item: StudentLearningWarningItem, index: number) => ({ - id: `${studentId}-${item.type ?? index}`, - level: item.isTriggered ? 'high' : 'low', - title: item.title || '学习预警', - content: item.description || item.statusName || '', - occurredAt: warningData.date || '', - isRead: !item.isTriggered, - isTriggered: Boolean(item.isTriggered), - statusName: item.statusName || '' - })) + const list: StudentAlert[] = visibleItems.map((item: StudentLearningWarningItem, index: number) => { + const warningType = learningWarningTypeMap[Number(item.type)] + return { + id: `${studentId}-${item.type ?? index}`, + level: item.isTriggered ? warningType?.level ?? 'high' : 'low', + title: item.title || warningType?.title || '学习预警', + content: item.description || item.statusName || '', + occurredAt: warningData.date || '', + isRead: !item.isTriggered, + isTriggered: Boolean(item.isTriggered), + statusName: item.statusName || (item.isTriggered ? '已触发' : '正常') + } + }) return { code: res.code, message: res.message, @@ -684,7 +705,69 @@ export function apiGetStudentVocabulary(studentId: string, showLoading: boolean } export function apiGetStudentGrowth(studentId: string, showLoading: boolean = true): Promise | null> { - return get(`/api/parent/growth?studentId=${studentId}`, undefined, { showLoading }) + return get(`/api/v1/StudentPoints/GetStudentPointSummary/${studentId}`, undefined, { showLoading }).then((res) => { + if (!res || !res.data) return null + return { + code: res.code, + message: res.message, + data: { + studentId: String(res.data.userId ?? studentId), + studentName: '', + studentGrade: '', + studentClass: '', + pointsBalance: Number(res.data.totalPoints) || 0, + level: Number(res.data.level) || 0, + rank: Number(res.data.classRank) || 0, + streakDays: Number(res.data.noPointDays) || 0, + totalPoints: Number(res.data.todayPoints) || 0, + records: [] + } + } + }) +} + +/** + * 分页获取学生积分流水。 + */ +export function apiGetStudentPointRecords(studentId: string, pageIndex: number = 1, pageSize: number = 20, prevPageId?: number, showLoading: boolean = true): Promise | null> { + const prevPageQuery = prevPageId === undefined || prevPageId === null ? '' : `&PrevPageId=${prevPageId}` + return get(`/api/v1/StudentPoints/GetStudentPointRecords?UserId=${studentId}&PageIndex=${pageIndex}&PageSize=${pageSize}${prevPageQuery}`, undefined, { showLoading }).then((res) => { + if (!res || !res.data) return null + const page = res.data + const records: GrowthPointRecord[] = (Array.isArray(page.data) ? page.data : []).map((item: any) => ({ + id: String(item.id), + title: item.pointMessage || '积分变动', + timeText: formatPointRecordTime(item.bizTime), + points: Number(item.points) || 0, + icon: pointRecordIcon(Number(item.type)) + })) + return { + code: res.code, + message: res.message, + data: { + records, + total: Number(page.total) || 0, + pageIndex: Number(page.pageIndex) || pageIndex, + pageSize: Number(page.pageSize) || pageSize, + lastId: page.id === null || page.id === undefined ? null : Number(page.id) + } + } + }) +} + +function pointRecordIcon(type: number): GrowthPointRecord['icon'] { + if (type === 1) return 'calendar' + if (type === 2 || type === 3) return 'assignment' + if (type === 4) return 'streak' + if (type === 5 || type === 6) return 'accuracy' + if (type === 7 || type === 8) return 'mastery' + return 'note' +} + +function formatPointRecordTime(value: any): string { + if (!value) return '--' + const text = String(value) + return text.length >= 16 ? text.substring(5, 16).replace('T', ' ') : text } export function apiGetStudentLearningTasks(studentId: string, showLoading: boolean = true): Promise | null> { diff --git a/pages/growth/index.uvue b/pages/growth/index.uvue index 3ccfbb8..b94fb40 100644 --- a/pages/growth/index.uvue +++ b/pages/growth/index.uvue @@ -1,18 +1,24 @@ @@ -81,4 +119,6 @@ .flow-title { font-size: 13px; color: #242b3d; } .flow-time { margin-top: 3px; font-size: 10px; color: #94a0b1; } .flow-points { font-size: 13px; font-weight: 700; color: #f29900; } + .flow-empty, .records-footer { display: flex; flex-direction: column; align-items: center; padding: 14px 0 4px; } + .flow-empty-text, .records-footer-text { font-size: 11px; color: #94a0b1; } diff --git a/pages/index/index.uvue b/pages/index/index.uvue index 290444e..fb05b5b 100644 --- a/pages/index/index.uvue +++ b/pages/index/index.uvue @@ -228,10 +228,10 @@ } } } - if (student.value.id) { + if (selectedId) { await Promise.all([ - loadStudentHomeMetrics(selectedId || student.value.id, showLoading), - loadStudentAlerts(selectedId || student.value.id, showLoading) + loadStudentHomeMetrics(selectedId, showLoading), + loadStudentAlerts(selectedId, showLoading) ]) } } diff --git a/utils/request.ts b/utils/request.ts index 40262b1..235a7b6 100644 --- a/utils/request.ts +++ b/utils/request.ts @@ -3,14 +3,12 @@ * 支持请求拦截、响应拦截、Loading显示、错误处理 */ -// 响应数据类型 export type ApiResponse = { code: number message: string data: T | null } -// 请求配置类型 export type RequestConfig = { url: string method?: 'GET' | 'POST' | 'PUT' | 'DELETE' @@ -21,37 +19,19 @@ export type RequestConfig = { showError?: boolean } -// 基础URL配置 - 虚拟接口 const BASE_URL = 'https://localhost:7145' - -// 请求超时时间 const TIMEOUT = 10000 - -// 联调前统一使用测试响应,保证所有页面流程可完整验证。 const USE_MOCK_API = true - -// Mock 接口模拟正常网络环境,响应延迟范围为 0 到 2 秒。 const MOCK_RESPONSE_MAX_DELAY = 2000 - -// Loading引用计数 let loadingCount = 0 -/** - * 显示Loading - */ function showLoading(text: string = '加载中...') { loadingCount++ if (loadingCount === 1) { - uni.showLoading({ - title: text, - mask: true - }) + uni.showLoading({ title: text, mask: true }) } } -/** - * 隐藏Loading - */ function hideLoading() { loadingCount-- if (loadingCount <= 0) { @@ -60,36 +40,19 @@ function hideLoading() { } } -/** - * 获取Token - */ function getToken(): string { return uni.getStorageSync('token') as string ?? '' } -/** - * 请求拦截器 - 添加token等 - */ function requestInterceptor(config: RequestConfig): RequestConfig { - // 添加完整URL - if (!config.url.startsWith('http')) { - config.url = BASE_URL + config.url - } - - // 添加请求头 + if (!config.url.startsWith('http')) config.url = BASE_URL + config.url + const token = getToken() config.header = config.header ?? {} config.header['Content-Type'] = 'application/json' - - if (token) { - config.header['Authorization'] = `Bearer ${token}` - } - - // 显示Loading - if (config.showLoading !== false) { - showLoading(config.loadingText) - } - + if (token) config.header['Authorization'] = `Bearer ${token}` + + if (config.showLoading !== false) showLoading(config.loadingText) return config } @@ -97,17 +60,13 @@ function requestInterceptor(config: RequestConfig): RequestConfig { * 响应拦截器 - 统一处理响应 */ function responseInterceptor(response: any, config: RequestConfig): ApiResponse | null { - // 隐藏Loading - if (config.showLoading !== false) { - hideLoading() - } - - // HTTP状态码检查 + if (config.showLoading !== false) hideLoading() + if (response.statusCode !== 200) { handleError(response.statusCode, '网络请求失败', config.showError) return null } - + // 解析响应数据,兼容后端 PascalCase 与前端 camelCase 两种包装格式。 const rawResult = response.data as any const result: ApiResponse = { @@ -115,50 +74,35 @@ function responseInterceptor(response: any, config: RequestConfig): ApiRespon message: String(rawResult?.message ?? rawResult?.Message ?? ''), data: (rawResult?.data ?? rawResult?.Data ?? null) as T | null } - - // 业务状态码检查 + if (result.code !== 200) { handleError(result.code, result.message, config.showError) return null } - return result } -/** - * 错误处理 - */ function handleError(code: number, message: string, showError: boolean = true) { console.error(`[Request Error] Code: ${code}, Message: ${message}`) - - if (!showError) { - return - } - - // 根据错误码显示不同提示 + if (!showError) return + let errorMsg = message - if (code === 401) { errorMsg = '登录已过期,请重新登录' - // 清除token并跳转登录页 uni.removeStorageSync('token') uni.removeStorageSync('userInfo') - uni.reLaunch({ - url: '/pages/login/index' - }) + uni.reLaunch({ url: '/pages/login/index' }) } else if (code === 403) { errorMsg = '没有权限访问' } else if (code === 404) { errorMsg = '请求的资源不存在' } else if (code === 500) { errorMsg = '服务器错误,请稍后重试' + } else if (code === -1) { + errorMsg = '网络异常' } - - uni.showToast({ - title: errorMsg, - icon: 'none', - duration: 2000 - }) + + uni.showToast({ title: errorMsg, icon: 'none', duration: 2000 }) } /** @@ -386,8 +330,7 @@ function createMockResponse(config: RequestConfig): ApiResponse { * 通用请求方法 */ export function request(config: RequestConfig): Promise | null> { - return new Promise((resolve, reject) => { - // 请求拦截 + return new Promise((resolve) => { const finalConfig = requestInterceptor(config) if (USE_MOCK_API && !finalConfig.url.includes('/api/v1/Parents/') && !finalConfig.url.includes('/api/v1/ParentStudentToday/') && !finalConfig.url.includes('/api/v1/ParentLearningWarning/') && !finalConfig.url.includes('/api/v1/ParentStudyPack/') && !finalConfig.url.includes('/api/v1/ParentLearningReport/')) { @@ -408,70 +351,28 @@ export function request(config: RequestConfig): Promise | null data: finalConfig.data, header: finalConfig.header, timeout: TIMEOUT, - success: (res) => { - // 响应拦截 - const result = responseInterceptor(res, finalConfig) - resolve(result) - }, + success: (res) => resolve(responseInterceptor(res, finalConfig)), fail: (err) => { - // 隐藏Loading - if (finalConfig.showLoading !== false) { - hideLoading() - } - - // 网络错误处理 + if (finalConfig.showLoading !== false) hideLoading() handleError(-1, err.errMsg ?? '网络连接失败', finalConfig.showError) - // 将网络失败转换为统一的空响应,避免页面事件中出现未处理 Promise 异常。 resolve(null) } }) }) } -/** - * GET请求 - */ export function get(url: string, data?: any, config?: Partial): Promise | null> { - return request({ - url, - method: 'GET', - data, - ...config - }) + return request({ url, method: 'GET', data, ...config }) } -/** - * POST请求 - */ export function post(url: string, data?: any, config?: Partial): Promise | null> { - return request({ - url, - method: 'POST', - data, - ...config - }) + return request({ url, method: 'POST', data, ...config }) } -/** - * PUT请求 - */ export function put(url: string, data?: any, config?: Partial): Promise | null> { - return request({ - url, - method: 'PUT', - data, - ...config - }) + return request({ url, method: 'PUT', data, ...config }) } -/** - * DELETE请求 - */ export function del(url: string, data?: any, config?: Partial): Promise | null> { - return request({ - url, - method: 'DELETE', - data, - ...config - }) + return request({ url, method: 'DELETE', data, ...config }) }