Skip to content

Commit fc92725

Browse files
committed
feat(analytics): add analytics tasks feature with UI integration and API support
1 parent 70b9897 commit fc92725

7 files changed

Lines changed: 473 additions & 0 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
<template>
2+
<v-card flat rounded="lg" class="border">
3+
<v-card-title class="pb-2">
4+
{{ title }}
5+
</v-card-title>
6+
<v-card-text>
7+
<div v-if="chartType === 'pie'" class="pie-chart">
8+
<svg :width="size" :height="size" viewBox="0 0 200 200">
9+
<g transform="translate(100, 100)">
10+
<path
11+
v-for="(segment, index) in pieSegments"
12+
:key="index"
13+
:d="segment.path"
14+
:fill="colors[index % colors.length]"
15+
:stroke="'white'"
16+
:stroke-width="2"
17+
/>
18+
</g>
19+
</svg>
20+
</div>
21+
22+
<div v-else-if="chartType === 'bar'" class="bar-chart">
23+
<svg :width="size" :height="size" viewBox="0 0 400 200">
24+
<g transform="translate(40, 160)">
25+
<rect
26+
v-for="(item, index) in data"
27+
:key="index"
28+
:x="index * barWidth"
29+
:y="-item.percentage * 1.5"
30+
:width="barWidth - 10"
31+
:height="item.percentage * 1.5"
32+
:fill="colors[index % colors.length]"
33+
/>
34+
<text
35+
v-for="(item, index) in data"
36+
:key="`label-${index}`"
37+
:x="index * barWidth + (barWidth - 10) / 2"
38+
:y="20"
39+
text-anchor="middle"
40+
font-size="12"
41+
class="chart-label"
42+
>
43+
{{ item.label }}
44+
</text>
45+
</g>
46+
</svg>
47+
</div>
48+
49+
<div class="legend mt-4">
50+
<div v-for="(item, index) in data" :key="index" class="legend-item">
51+
<div class="legend-color" :style="{ backgroundColor: colors[index % colors.length] }" />
52+
<span class="legend-label">{{ item.label }}</span>
53+
<span class="legend-value">{{ item.count }} ({{ item.percentage.toFixed(1) }}%)</span>
54+
</div>
55+
</div>
56+
</v-card-text>
57+
</v-card>
58+
</template>
59+
60+
<script setup lang="ts">
61+
import type { AnalyticsDistributionItem } from '@/network/api/spaces/types'
62+
63+
import { computed } from 'vue'
64+
65+
interface Props {
66+
title: string
67+
data: AnalyticsDistributionItem[]
68+
chartType: 'pie' | 'bar'
69+
size?: number
70+
}
71+
72+
const props = withDefaults(defineProps<Props>(), {
73+
size: 200,
74+
})
75+
76+
const colors = [
77+
'#1976D2',
78+
'#2E7D32',
79+
'#F57C00',
80+
'#8E24AA',
81+
'#D32F2F',
82+
'#00796B',
83+
'#5E35B1',
84+
'#689F38',
85+
'#FF5722',
86+
'#795548',
87+
'#607D8B',
88+
'#E91E63',
89+
'#00BCD4',
90+
'#FF9800',
91+
'#9C27B0',
92+
'#4CAF50',
93+
]
94+
95+
const barWidth = computed(() => {
96+
return Math.min(320 / props.data.length, 60)
97+
})
98+
99+
const pieSegments = computed(() => {
100+
if (props.chartType !== 'pie') return []
101+
102+
let cumulativeAngle = 0
103+
return props.data.map((item) => {
104+
const angle = (item.percentage / 100) * 2 * Math.PI
105+
const startAngle = cumulativeAngle
106+
const endAngle = cumulativeAngle + angle
107+
108+
const x1 = Math.cos(startAngle) * 80
109+
const y1 = Math.sin(startAngle) * 80
110+
const x2 = Math.cos(endAngle) * 80
111+
const y2 = Math.sin(endAngle) * 80
112+
113+
const largeArcFlag = angle > Math.PI ? 1 : 0
114+
115+
const path = ['M', 0, 0, 'L', x1, y1, 'A', 80, 80, 0, largeArcFlag, 1, x2, y2, 'Z'].join(' ')
116+
117+
cumulativeAngle += angle
118+
119+
return { path }
120+
})
121+
})
122+
</script>
123+
124+
<style scoped>
125+
.border {
126+
border: 1px solid rgba(var(--v-border-color), 0.12);
127+
}
128+
129+
.pie-chart,
130+
.bar-chart {
131+
display: flex;
132+
justify-content: center;
133+
align-items: center;
134+
}
135+
136+
.chart-label {
137+
fill: rgba(var(--v-theme-on-surface), 0.87);
138+
}
139+
140+
.legend {
141+
display: flex;
142+
flex-direction: column;
143+
gap: 8px;
144+
max-height: 200px;
145+
overflow-y: auto;
146+
}
147+
148+
.legend-item {
149+
display: flex;
150+
align-items: center;
151+
gap: 8px;
152+
font-size: 0.875rem;
153+
}
154+
155+
.legend-color {
156+
width: 12px;
157+
height: 12px;
158+
border-radius: 2px;
159+
flex-shrink: 0;
160+
}
161+
162+
.legend-label {
163+
flex: 1;
164+
color: rgba(var(--v-theme-on-surface), 0.87);
165+
}
166+
167+
.legend-value {
168+
color: rgba(var(--v-theme-on-surface), 0.6);
169+
font-weight: 500;
170+
}
171+
</style>

src/components/spaces/SpaceSidebar.vue

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,24 @@ const spaceId = computed(() => Number(route.params.spaceId))
172172
<span>{{ t('spaces.detail.manageTemplates.title') }}</span>
173173
</v-tooltip>
174174

175+
<v-tooltip :disabled="expanded" location="end">
176+
<template #activator="{ props }">
177+
<v-list-item
178+
v-bind="expanded ? {} : props"
179+
rounded="lg"
180+
:to="{ name: 'SpacesDetailAnalyticsTasks', params: { spaceId: spaceId } }"
181+
color="primary"
182+
class="sidebar-item"
183+
>
184+
<template #prepend>
185+
<v-icon>mdi-chart-line</v-icon>
186+
</template>
187+
<v-list-item-title v-show="expanded">{{ t('spaces.detail.analytics.title') }}</v-list-item-title>
188+
</v-list-item>
189+
</template>
190+
<span>{{ t('spaces.detail.analytics.title') }}</span>
191+
</v-tooltip>
192+
175193
<v-tooltip :disabled="expanded" location="end">
176194
<template #activator="{ props }">
177195
<v-list-item

src/i18n/messages/zh-CN/spaces.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@
113113
"categoryNameMaxLength": "分类名称最多32个字符",
114114
"descriptionMaxLength": "描述最多255个字符"
115115
},
116+
"analytics": {
117+
"title": "数据分析"
118+
},
116119
"publishTask": {
117120
"title": "发布赛题",
118121
"back": "返回",

src/network/api/spaces/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
PostSpaceAdminRequestData,
88
PostSpaceCategoryRequestData,
99
PostSpaceRequestData,
10+
SpaceAnalyticsTasksData,
1011
} from './types'
1112

1213
import { NewApiInstance } from '../index'
@@ -49,6 +50,12 @@ export namespace SpacesApi {
4950
params,
5051
})
5152

53+
export const getAnalyticsTasks = (spaceId: number) =>
54+
NewApiInstance.request<SpaceAnalyticsTasksData>({
55+
url: `/spaces/${spaceId}/analytics/tasks`,
56+
method: 'GET',
57+
})
58+
5259
export const addAdmin = (spaceId: number, data: PostSpaceAdminRequestData) =>
5360
NewApiInstance.request<{ space: Space }>({
5461
url: `/spaces/${spaceId}/managers`,

src/network/api/spaces/types.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,35 @@ export type PostSpaceCategoryRequestData = {
3939
}
4040

4141
export type PatchSpaceCategoryRequestData = Partial<PostSpaceCategoryRequestData>
42+
43+
// Analytics Types
44+
export type AnalyticsDistributionItem = {
45+
count: number
46+
percentage: number
47+
label: string
48+
rangeStart: null | number
49+
rangeEnd: null | number
50+
}
51+
52+
export type AnalyticsDistribution = {
53+
name: string
54+
type: 'DISCRETE' | 'CONTINUOUS'
55+
items: AnalyticsDistributionItem[]
56+
}
57+
58+
export type AnalyticsStudentStatistics = {
59+
totalStudents: number
60+
totalStudentsWithRealName: number
61+
gradeDistribution: AnalyticsDistribution
62+
majorDistribution: AnalyticsDistribution
63+
classNameDistribution: AnalyticsDistribution
64+
}
65+
66+
export type SpaceAnalyticsTasksData = {
67+
taskCategoryDistribution: AnalyticsDistribution
68+
taskStatusDistribution: AnalyticsDistribution
69+
participantStatusDistribution: AnalyticsDistribution
70+
successStudentStatistics: AnalyticsStudentStatistics
71+
unsuccessStudentStatistics: AnalyticsStudentStatistics
72+
rankDistribution: AnalyticsDistribution
73+
}

src/router/spaces.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ export default {
5454
name: 'SpacesDetailSelectTemplate',
5555
component: () => import('@/views/spaces/detail/SelectTemplate.vue'),
5656
},
57+
{
58+
path: 'analytics/tasks',
59+
name: 'SpacesDetailAnalyticsTasks',
60+
component: () => import('@/views/spaces/detail/AnalyticsTasks.vue'),
61+
},
5762
{
5863
path: 'manage/topics',
5964
name: 'SpacesDetailManageTopics',

0 commit comments

Comments
 (0)