Compare commits
2 Commits
846ba61b88
...
95b3e973e0
| Author | SHA1 | Date | |
|---|---|---|---|
| 95b3e973e0 | |||
| 6af88225c9 |
@ -97,6 +97,17 @@ export interface UpdateTenantQuotaDto {
|
|||||||
studentQuota?: number;
|
studentQuota?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 后端返回的统计数据结构
|
||||||
|
export interface AdminStatsResponse {
|
||||||
|
totalTenants: number;
|
||||||
|
activeTenants: number;
|
||||||
|
totalCourses: number;
|
||||||
|
totalStudents: number;
|
||||||
|
totalTeachers: number;
|
||||||
|
totalLessons: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前端使用的统计数据结构
|
||||||
export interface AdminStats {
|
export interface AdminStats {
|
||||||
tenantCount: number;
|
tenantCount: number;
|
||||||
activeTenantCount: number;
|
activeTenantCount: number;
|
||||||
@ -108,6 +119,15 @@ export interface AdminStats {
|
|||||||
monthlyLessons: number;
|
monthlyLessons: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 后端返回的趋势数据结构(分离数组格式)
|
||||||
|
export interface StatsTrendResponse {
|
||||||
|
dates: string[];
|
||||||
|
newStudents: number[];
|
||||||
|
newTeachers: number[];
|
||||||
|
newCourses: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前端使用的趋势数据结构
|
||||||
export interface TrendData {
|
export interface TrendData {
|
||||||
month: string;
|
month: string;
|
||||||
tenantCount: number;
|
tenantCount: number;
|
||||||
@ -115,14 +135,32 @@ export interface TrendData {
|
|||||||
studentCount: number;
|
studentCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 后端返回的活跃租户结构
|
||||||
|
export interface ActiveTenantResponse {
|
||||||
|
tenantId: number;
|
||||||
|
tenantName: string;
|
||||||
|
activeUsers: number;
|
||||||
|
courseCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前端使用的活跃租户结构
|
||||||
export interface ActiveTenant {
|
export interface ActiveTenant {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
lessonCount: number;
|
lessonCount: number;
|
||||||
teacherCount: number;
|
teacherCount: number | string;
|
||||||
studentCount: number;
|
studentCount: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 后端返回的热门课程结构
|
||||||
|
export interface PopularCourseResponse {
|
||||||
|
courseId: number;
|
||||||
|
courseName: string;
|
||||||
|
usageCount: number;
|
||||||
|
teacherCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前端使用的热门课程结构
|
||||||
export interface PopularCourse {
|
export interface PopularCourse {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@ -211,17 +249,71 @@ export const deleteTenant = (id: number) =>
|
|||||||
|
|
||||||
// ==================== 统计数据 ====================
|
// ==================== 统计数据 ====================
|
||||||
|
|
||||||
export const getAdminStats = () =>
|
// 数据映射函数:后端 -> 前端
|
||||||
http.get<AdminStats>('/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<TrendData[]>('/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<ActiveTenant[]>('/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<PopularCourse[]>('/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<AdminStatsResponse>('/v1/admin/stats');
|
||||||
|
return mapStatsData(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTrendData = async () => {
|
||||||
|
const data = await http.get<StatsTrendResponse>('/v1/admin/stats/trend');
|
||||||
|
return mapTrendData(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getActiveTenants = async (limit?: number) => {
|
||||||
|
const data = await http.get<ActiveTenantResponse[]>('/v1/admin/stats/tenants/active', { params: { limit } });
|
||||||
|
return mapActiveTenants(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPopularCourses = async (limit?: number) => {
|
||||||
|
const data = await http.get<PopularCourseResponse[]>('/v1/admin/stats/courses/popular', { params: { limit } });
|
||||||
|
return mapPopularCourses(data);
|
||||||
|
};
|
||||||
|
|
||||||
// ==================== 课程套餐 ====================
|
// ==================== 课程套餐 ====================
|
||||||
|
|
||||||
|
|||||||
25
reading-platform-frontend/src/components.d.ts
vendored
25
reading-platform-frontend/src/components.d.ts
vendored
@ -7,16 +7,11 @@ export {}
|
|||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AAlert: typeof import('ant-design-vue/es')['Alert']
|
|
||||||
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
||||||
ABadge: typeof import('ant-design-vue/es')['Badge']
|
ABadge: typeof import('ant-design-vue/es')['Badge']
|
||||||
AButton: typeof import('ant-design-vue/es')['Button']
|
AButton: typeof import('ant-design-vue/es')['Button']
|
||||||
AButtonGroup: typeof import('ant-design-vue/es')['ButtonGroup']
|
|
||||||
ACard: typeof import('ant-design-vue/es')['Card']
|
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']
|
ACol: typeof import('ant-design-vue/es')['Col']
|
||||||
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
|
|
||||||
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
|
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
|
||||||
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
|
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
|
||||||
ADivider: typeof import('ant-design-vue/es')['Divider']
|
ADivider: typeof import('ant-design-vue/es')['Divider']
|
||||||
@ -26,7 +21,6 @@ declare module 'vue' {
|
|||||||
AForm: typeof import('ant-design-vue/es')['Form']
|
AForm: typeof import('ant-design-vue/es')['Form']
|
||||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
||||||
AImage: typeof import('ant-design-vue/es')['Image']
|
AImage: typeof import('ant-design-vue/es')['Image']
|
||||||
AImagePreviewGroup: typeof import('ant-design-vue/es')['ImagePreviewGroup']
|
|
||||||
AInput: typeof import('ant-design-vue/es')['Input']
|
AInput: typeof import('ant-design-vue/es')['Input']
|
||||||
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
|
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
|
||||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||||
@ -35,41 +29,22 @@ declare module 'vue' {
|
|||||||
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
|
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
|
||||||
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
|
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
|
||||||
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
|
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']
|
AMenu: typeof import('ant-design-vue/es')['Menu']
|
||||||
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
|
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
|
||||||
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
||||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||||
APageHeader: typeof import('ant-design-vue/es')['PageHeader']
|
APageHeader: typeof import('ant-design-vue/es')['PageHeader']
|
||||||
APagination: typeof import('ant-design-vue/es')['Pagination']
|
|
||||||
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
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']
|
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
|
||||||
ARate: typeof import('ant-design-vue/es')['Rate']
|
|
||||||
ARow: typeof import('ant-design-vue/es')['Row']
|
ARow: typeof import('ant-design-vue/es')['Row']
|
||||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||||
ASelectOptGroup: typeof import('ant-design-vue/es')['SelectOptGroup']
|
|
||||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||||
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
|
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
|
||||||
ASpace: typeof import('ant-design-vue/es')['Space']
|
ASpace: typeof import('ant-design-vue/es')['Space']
|
||||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
|
||||||
AStatistic: typeof import('ant-design-vue/es')['Statistic']
|
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']
|
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']
|
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
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']
|
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||||
FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default']
|
FilePreviewModal: typeof import('./components/FilePreviewModal.vue')['default']
|
||||||
FileUploader: typeof import('./components/course/FileUploader.vue')['default']
|
FileUploader: typeof import('./components/course/FileUploader.vue')['default']
|
||||||
|
|||||||
@ -82,9 +82,9 @@
|
|||||||
<div class="rank-number" :class="'rank-' + (index + 1)">{{ index + 1 }}</div>
|
<div class="rank-number" :class="'rank-' + (index + 1)">{{ index + 1 }}</div>
|
||||||
<div class="rank-content">
|
<div class="rank-content">
|
||||||
<span class="rank-name">{{ item.name }}</span>
|
<span class="rank-name">{{ item.name }}</span>
|
||||||
<span class="rank-desc">教师: {{ item.teacherCount }} | 学生: {{ item.studentCount }}</span>
|
<span class="rank-desc">活跃用户: {{ item.lessonCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
<a-tag color="blue">{{ item.lessonCount }} 次</a-tag>
|
<a-tag color="blue">{{ item.lessonCount }} 课程</a-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a-empty v-else description="暂无数据" />
|
<a-empty v-else description="暂无数据" />
|
||||||
|
|||||||
@ -392,6 +392,13 @@ const initTrendChart = (data: LessonTrendItem[]) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保数据格式正确
|
||||||
|
const validData = data.map(d => ({
|
||||||
|
month: d.month || '',
|
||||||
|
lessonCount: d.lessonCount || 0,
|
||||||
|
studentCount: d.studentCount || 0,
|
||||||
|
}));
|
||||||
|
|
||||||
const option: echarts.EChartsOption = {
|
const option: echarts.EChartsOption = {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
@ -419,7 +426,7 @@ const initTrendChart = (data: LessonTrendItem[]) => {
|
|||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: data.map((d) => d.month),
|
data: validData.map((d) => d.month),
|
||||||
axisLine: {
|
axisLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: '#E5E7EB',
|
color: '#E5E7EB',
|
||||||
@ -450,7 +457,7 @@ const initTrendChart = (data: LessonTrendItem[]) => {
|
|||||||
{
|
{
|
||||||
name: '授课次数',
|
name: '授课次数',
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: data.map((d) => d.lessonCount),
|
data: validData.map((d) => d.lessonCount),
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
{ offset: 0, color: '#FF8C42' },
|
{ offset: 0, color: '#FF8C42' },
|
||||||
@ -463,7 +470,7 @@ const initTrendChart = (data: LessonTrendItem[]) => {
|
|||||||
name: '学生数',
|
name: '学生数',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
yAxisIndex: 1,
|
yAxisIndex: 1,
|
||||||
data: data.map((d) => d.studentCount),
|
data: validData.map((d) => d.studentCount),
|
||||||
smooth: true,
|
smooth: true,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: '#667eea',
|
color: '#667eea',
|
||||||
@ -491,6 +498,40 @@ const initDistributionChart = (data: CourseDistributionItem[]) => {
|
|||||||
|
|
||||||
distributionChart = echarts.init(distributionChartRef.value);
|
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 = {
|
const option: echarts.EChartsOption = {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'item',
|
trigger: 'item',
|
||||||
@ -526,21 +567,7 @@ const initDistributionChart = (data: CourseDistributionItem[]) => {
|
|||||||
labelLine: {
|
labelLine: {
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
data: data.map((item, index) => ({
|
data: validData,
|
||||||
...item,
|
|
||||||
itemStyle: {
|
|
||||||
color: [
|
|
||||||
'#FF8C42',
|
|
||||||
'#667eea',
|
|
||||||
'#f093fb',
|
|
||||||
'#4facfe',
|
|
||||||
'#43e97b',
|
|
||||||
'#fa709a',
|
|
||||||
'#fee140',
|
|
||||||
'#30cfd0',
|
|
||||||
][index % 8],
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -200,7 +200,7 @@
|
|||||||
<div v-else class="cover-placeholder">
|
<div v-else class="cover-placeholder">
|
||||||
<BookFilled />
|
<BookFilled />
|
||||||
</div>
|
</div>
|
||||||
<div class="duration-tag">{{ course.duration }}分钟</div>
|
<div class="duration-tag">{{ course.duration || 30 }}分钟</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="recommend-info">
|
<div class="recommend-info">
|
||||||
<div class="recommend-name">{{ course.name }}</div>
|
<div class="recommend-name">{{ course.name }}</div>
|
||||||
@ -637,9 +637,34 @@ const loadDashboard = async () => {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const data = await getTeacherDashboard();
|
const data = await getTeacherDashboard();
|
||||||
stats.value = data.stats;
|
stats.value = data.stats || { classCount: 0, studentCount: 0, lessonCount: 0, courseCount: 0 };
|
||||||
todayLessons.value = data.todayLessons || [];
|
|
||||||
recommendedCourses.value = data.recommendedCourses || [];
|
// 映射今日课程数据,处理可能缺失的字段
|
||||||
|
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) => ({
|
recentActivities.value = (data.recentActivities || []).map((item: RecentActivity) => ({
|
||||||
...item,
|
...item,
|
||||||
time: formatActivityTime(item.time),
|
time: formatActivityTime(item.time),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user