diff --git a/reading-platform-frontend/src/api/admin.ts b/reading-platform-frontend/src/api/admin.ts index c6a5975..35341c6 100644 --- a/reading-platform-frontend/src/api/admin.ts +++ b/reading-platform-frontend/src/api/admin.ts @@ -97,6 +97,17 @@ export interface UpdateTenantQuotaDto { studentQuota?: number; } +// 后端返回的统计数据结构 +export interface AdminStatsResponse { + totalTenants: number; + activeTenants: number; + totalCourses: number; + totalStudents: number; + totalTeachers: number; + totalLessons: number; +} + +// 前端使用的统计数据结构 export interface AdminStats { tenantCount: number; activeTenantCount: number; @@ -108,6 +119,15 @@ export interface AdminStats { monthlyLessons: number; } +// 后端返回的趋势数据结构(分离数组格式) +export interface StatsTrendResponse { + dates: string[]; + newStudents: number[]; + newTeachers: number[]; + newCourses: number[]; +} + +// 前端使用的趋势数据结构 export interface TrendData { month: string; tenantCount: number; @@ -115,14 +135,32 @@ export interface TrendData { studentCount: number; } +// 后端返回的活跃租户结构 +export interface ActiveTenantResponse { + tenantId: number; + tenantName: string; + activeUsers: number; + courseCount: number; +} + +// 前端使用的活跃租户结构 export interface ActiveTenant { id: number; name: string; lessonCount: number; - teacherCount: number; - studentCount: number; + teacherCount: number | string; + studentCount: number | string; } +// 后端返回的热门课程结构 +export interface PopularCourseResponse { + courseId: number; + courseName: string; + usageCount: number; + teacherCount: number; +} + +// 前端使用的热门课程结构 export interface PopularCourse { id: number; name: string; @@ -211,17 +249,71 @@ export const deleteTenant = (id: number) => // ==================== 统计数据 ==================== -export const getAdminStats = () => - http.get('/v1/admin/stats'); +// 数据映射函数:后端 -> 前端 +const mapStatsData = (data: AdminStatsResponse): AdminStats => ({ + tenantCount: data.totalTenants || 0, + activeTenantCount: data.activeTenants || 0, + courseCount: data.totalCourses || 0, + studentCount: data.totalStudents || 0, + teacherCount: data.totalTeachers || 0, + lessonCount: data.totalLessons || 0, + publishedCourseCount: 0, // 后端暂无此数据 + monthlyLessons: 0, // 后端暂无此数据 +}); -export const getTrendData = () => - http.get('/v1/admin/stats/trend'); +// 趋势数据映射:分离数组 -> 对象数组 +const mapTrendData = (data: StatsTrendResponse): TrendData[] => { + if (!data || !data.dates || data.dates.length === 0) return []; + return data.dates.map((date, index) => ({ + month: date, + tenantCount: 0, // 后端无此数据 + lessonCount: data.newCourses?.[index] || 0, + studentCount: data.newStudents?.[index] || 0, + })); +}; -export const getActiveTenants = (limit?: number) => - http.get('/v1/admin/stats/tenants/active', { params: { limit } }); +// 活跃租户数据映射 +const mapActiveTenants = (data: ActiveTenantResponse[]): ActiveTenant[] => { + if (!data || data.length === 0) return []; + return data.map(item => ({ + id: item.tenantId, + name: item.tenantName, + teacherCount: '-', // 后端无单独字段 + studentCount: '-', // 后端无单独字段 + lessonCount: item.courseCount, // 使用 courseCount 替代 + })); +}; -export const getPopularCourses = (limit?: number) => - http.get('/v1/admin/stats/courses/popular', { params: { limit } }); +// 热门课程数据映射 +const mapPopularCourses = (data: PopularCourseResponse[]): PopularCourse[] => { + if (!data || data.length === 0) return []; + return data.map(item => ({ + id: item.courseId, + name: item.courseName, + usageCount: item.usageCount, + teacherCount: item.teacherCount, + })); +}; + +export const getAdminStats = async () => { + const data = await http.get('/v1/admin/stats'); + return mapStatsData(data); +}; + +export const getTrendData = async () => { + const data = await http.get('/v1/admin/stats/trend'); + return mapTrendData(data); +}; + +export const getActiveTenants = async (limit?: number) => { + const data = await http.get('/v1/admin/stats/tenants/active', { params: { limit } }); + return mapActiveTenants(data); +}; + +export const getPopularCourses = async (limit?: number) => { + const data = await http.get('/v1/admin/stats/courses/popular', { params: { limit } }); + return mapPopularCourses(data); +}; // ==================== 课程套餐 ==================== diff --git a/reading-platform-frontend/src/components.d.ts b/reading-platform-frontend/src/components.d.ts index 03a884c..6c38de9 100644 --- a/reading-platform-frontend/src/components.d.ts +++ b/reading-platform-frontend/src/components.d.ts @@ -7,16 +7,11 @@ export {} declare module 'vue' { export interface GlobalComponents { - AAlert: typeof import('ant-design-vue/es')['Alert'] AAvatar: typeof import('ant-design-vue/es')['Avatar'] ABadge: typeof import('ant-design-vue/es')['Badge'] AButton: typeof import('ant-design-vue/es')['Button'] - AButtonGroup: typeof import('ant-design-vue/es')['ButtonGroup'] ACard: typeof import('ant-design-vue/es')['Card'] - ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] - ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup'] ACol: typeof import('ant-design-vue/es')['Col'] - ADatePicker: typeof import('ant-design-vue/es')['DatePicker'] ADescriptions: typeof import('ant-design-vue/es')['Descriptions'] ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem'] ADivider: typeof import('ant-design-vue/es')['Divider'] @@ -26,7 +21,6 @@ declare module 'vue' { AForm: typeof import('ant-design-vue/es')['Form'] AFormItem: typeof import('ant-design-vue/es')['FormItem'] AImage: typeof import('ant-design-vue/es')['Image'] - AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup'] AInput: typeof import('ant-design-vue/es')['Input'] AInputNumber: typeof import('ant-design-vue/es')['InputNumber'] AInputPassword: typeof import('ant-design-vue/es')['InputPassword'] @@ -35,41 +29,22 @@ declare module 'vue' { ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent'] ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader'] ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider'] - AList: typeof import('ant-design-vue/es')['List'] - AListItem: typeof import('ant-design-vue/es')['ListItem'] - AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta'] AMenu: typeof import('ant-design-vue/es')['Menu'] AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider'] AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] AModal: typeof import('ant-design-vue/es')['Modal'] APageHeader: typeof import('ant-design-vue/es')['PageHeader'] - APagination: typeof import('ant-design-vue/es')['Pagination'] APopconfirm: typeof import('ant-design-vue/es')['Popconfirm'] - AProgress: typeof import('ant-design-vue/es')['Progress'] - ARadio: typeof import('ant-design-vue/es')['Radio'] - ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup'] ARangePicker: typeof import('ant-design-vue/es')['RangePicker'] - ARate: typeof import('ant-design-vue/es')['Rate'] ARow: typeof import('ant-design-vue/es')['Row'] ASelect: typeof import('ant-design-vue/es')['Select'] - ASelectOptGroup: typeof import('ant-design-vue/es')['SelectOptGroup'] ASelectOption: typeof import('ant-design-vue/es')['SelectOption'] ASkeleton: typeof import('ant-design-vue/es')['Skeleton'] ASpace: typeof import('ant-design-vue/es')['Space'] - ASpin: typeof import('ant-design-vue/es')['Spin'] AStatistic: typeof import('ant-design-vue/es')['Statistic'] - AStep: typeof import('ant-design-vue/es')['Step'] - ASteps: typeof import('ant-design-vue/es')['Steps'] - ASubMenu: typeof import('ant-design-vue/es')['SubMenu'] - ASwitch: typeof import('ant-design-vue/es')['Switch'] ATable: typeof import('ant-design-vue/es')['Table'] - ATabPane: typeof import('ant-design-vue/es')['TabPane'] - ATabs: typeof import('ant-design-vue/es')['Tabs'] ATag: typeof import('ant-design-vue/es')['Tag'] ATextarea: typeof import('ant-design-vue/es')['Textarea'] - ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker'] - ATooltip: typeof import('ant-design-vue/es')['Tooltip'] - ATypographyText: typeof import('ant-design-vue/es')['TypographyText'] AUpload: typeof import('ant-design-vue/es')['Upload'] FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default'] FileUploader: typeof import('./components/course/FileUploader.vue')['default'] diff --git a/reading-platform-frontend/src/views/admin/DashboardView.vue b/reading-platform-frontend/src/views/admin/DashboardView.vue index cbc8298..39596f0 100644 --- a/reading-platform-frontend/src/views/admin/DashboardView.vue +++ b/reading-platform-frontend/src/views/admin/DashboardView.vue @@ -82,9 +82,9 @@
{{ index + 1 }}
{{ item.name }} - 教师: {{ item.teacherCount }} | 学生: {{ item.studentCount }} + 活跃用户: {{ item.lessonCount }}
- {{ item.lessonCount }} 次 + {{ item.lessonCount }} 课程 diff --git a/reading-platform-frontend/src/views/school/DashboardView.vue b/reading-platform-frontend/src/views/school/DashboardView.vue index cd057cd..3816299 100644 --- a/reading-platform-frontend/src/views/school/DashboardView.vue +++ b/reading-platform-frontend/src/views/school/DashboardView.vue @@ -392,6 +392,13 @@ const initTrendChart = (data: LessonTrendItem[]) => { return; } + // 确保数据格式正确 + const validData = data.map(d => ({ + month: d.month || '', + lessonCount: d.lessonCount || 0, + studentCount: d.studentCount || 0, + })); + const option: echarts.EChartsOption = { tooltip: { trigger: 'axis', @@ -419,7 +426,7 @@ const initTrendChart = (data: LessonTrendItem[]) => { }, xAxis: { type: 'category', - data: data.map((d) => d.month), + data: validData.map((d) => d.month), axisLine: { lineStyle: { color: '#E5E7EB', @@ -450,7 +457,7 @@ const initTrendChart = (data: LessonTrendItem[]) => { { name: '授课次数', type: 'bar', - data: data.map((d) => d.lessonCount), + data: validData.map((d) => d.lessonCount), itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#FF8C42' }, @@ -463,7 +470,7 @@ const initTrendChart = (data: LessonTrendItem[]) => { name: '学生数', type: 'line', yAxisIndex: 1, - data: data.map((d) => d.studentCount), + data: validData.map((d) => d.studentCount), smooth: true, itemStyle: { color: '#667eea', @@ -491,6 +498,40 @@ const initDistributionChart = (data: CourseDistributionItem[]) => { distributionChart = echarts.init(distributionChartRef.value); + // 如果没有数据,显示空状态 + if (!data || data.length === 0) { + distributionChart.setOption({ + title: { + text: '暂无数据', + left: 'center', + top: 'center', + textStyle: { + color: '#999', + fontSize: 14, + }, + }, + }); + return; + } + + // 确保数据格式正确 + const validData = data.map((item, index) => ({ + name: item.name || `课程${index + 1}`, + value: item.value || 0, + itemStyle: { + color: [ + '#FF8C42', + '#667eea', + '#f093fb', + '#4facfe', + '#43e97b', + '#fa709a', + '#fee140', + '#30cfd0', + ][index % 8], + }, + })); + const option: echarts.EChartsOption = { tooltip: { trigger: 'item', @@ -526,21 +567,7 @@ const initDistributionChart = (data: CourseDistributionItem[]) => { labelLine: { show: false, }, - data: data.map((item, index) => ({ - ...item, - itemStyle: { - color: [ - '#FF8C42', - '#667eea', - '#f093fb', - '#4facfe', - '#43e97b', - '#fa709a', - '#fee140', - '#30cfd0', - ][index % 8], - }, - })), + data: validData, }, ], }; diff --git a/reading-platform-frontend/src/views/teacher/DashboardView.vue b/reading-platform-frontend/src/views/teacher/DashboardView.vue index 063ed7c..4d1a838 100644 --- a/reading-platform-frontend/src/views/teacher/DashboardView.vue +++ b/reading-platform-frontend/src/views/teacher/DashboardView.vue @@ -200,7 +200,7 @@
-
{{ course.duration }}分钟
+
{{ course.duration || 30 }}分钟
{{ course.name }}
@@ -637,9 +637,34 @@ const loadDashboard = async () => { loading.value = true; try { const data = await getTeacherDashboard(); - stats.value = data.stats; - todayLessons.value = data.todayLessons || []; - recommendedCourses.value = data.recommendedCourses || []; + stats.value = data.stats || { classCount: 0, studentCount: 0, lessonCount: 0, courseCount: 0 }; + + // 映射今日课程数据,处理可能缺失的字段 + todayLessons.value = (data.todayLessons || []).map((lesson: any) => ({ + id: lesson.id, + courseId: lesson.courseId, + courseName: lesson.courseName || lesson.course?.name || '未命名课程', + pictureBookName: lesson.pictureBookName, + classId: lesson.classId, + className: lesson.className || lesson.class?.name || '未命名班级', + plannedDatetime: lesson.plannedDatetime || lesson.startDatetime || lesson.lessonDate, + status: lesson.status, + duration: lesson.duration || lesson.actualDuration || 30, // 默认30分钟 + })); + + // 映射推荐课程数据,处理可能缺失的字段 + recommendedCourses.value = (data.recommendedCourses || []).map((course: any) => ({ + id: course.id, + name: course.name || '未命名课程', + pictureBookName: course.pictureBookName, + coverImagePath: course.coverImagePath, + duration: course.duration || 30, // 默认30分钟 + usageCount: course.usageCount || 0, + avgRating: course.avgRating || 0, // 默认0分 + gradeTags: course.gradeTags || [], + })); + + // 处理近期活动数据 recentActivities.value = (data.recentActivities || []).map((item: RecentActivity) => ({ ...item, time: formatActivityTime(item.time),