diff --git a/.trae/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc b/.trae/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc new file mode 100644 index 0000000..8cc30bd Binary files /dev/null and b/.trae/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc differ diff --git a/.trae/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-312.pyc b/.trae/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-312.pyc new file mode 100644 index 0000000..8341c0a Binary files /dev/null and b/.trae/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-312.pyc differ diff --git a/src/components/EnglishWordReport.vue b/src/components/EnglishWordReport.vue index 09c3b0f..2651c0c 100644 --- a/src/components/EnglishWordReport.vue +++ b/src/components/EnglishWordReport.vue @@ -64,10 +64,10 @@
- + - + @@ -85,9 +86,22 @@ style="width:120px; min-width: 100px;" size="large" :disabled="!selectedGrade" + clearable > + + + 重置 +
@@ -273,12 +287,25 @@
-

请确认或调整导出数据的筛选条件:

+
+ + + + + +

请确认或调整导出数据的筛选条件:

+
云校: @@ -464,6 +491,21 @@ const classList = [ { value: 'class4', label: '高二2班' }, ] +const hasActiveFilters = computed(() => { + return selectedCloud.value !== '' || + selectedSchool.value !== '' || + selectedGrade.value !== '' || + selectedClass.value !== '' +}) + +const resetFilters = () => { + selectedCloud.value = '' + selectedSchool.value = '' + selectedGrade.value = '' + selectedClass.value = '' + MessagePlugin.success('筛选条件已重置') +} + // ── 总体学情统计 ────────────────────────────────────────── const disabledFutureDate = (date) => { return dayjs(date).isAfter(dayjs().endOf('day')) @@ -939,11 +981,11 @@ const progressDistData = ref([ const progressColor = (range) => { const value = parseInt(range) - if (value >= 90) return '#00a870' - if (value >= 80) return '#0052d9' - if (value >= 60) return '#ed7b2f' - if (value >= 40) return '#f5a623' - return '#e34d59' + if (value >= 90) return '#10B981' + if (value >= 80) return '#3B82F6' + if (value >= 60) return '#F59E0B' + if (value >= 40) return '#F97316' + return '#EF4444' } const progressDistColumns = [ @@ -975,25 +1017,39 @@ const initProgressChart = () => { progressChart = echarts.init(progressChartRef.value) - // 翻转数据以使其在图表上从0到100显示,或者保持从高到低视需求而定 - // 这里我们让X轴从0%到100%排列 const chartData = [...progressDistData.value].reverse() const option = { tooltip: { trigger: 'axis', - axisPointer: { type: 'shadow' }, - formatter: '{b}
学生人数: {c}人', - backgroundColor: 'rgba(255, 255, 255, 0.95)', - borderColor: '#e2e8f0', + axisPointer: { + type: 'shadow', + shadowStyle: { + color: 'rgba(30, 64, 175, 0.08)' + } + }, + formatter: (params) => { + const data = params[0] + return `
+
${data.name}
+
+ + 学生人数: + ${data.value}人 +
+
` + }, + backgroundColor: 'rgba(255, 255, 255, 0.98)', + borderColor: 'rgba(30, 64, 175, 0.15)', + borderWidth: 1, textStyle: { color: '#1e293b' }, - extraCssText: 'box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); border-radius: 8px;' + extraCssText: 'box-shadow: 0 10px 25px rgba(0, 0, 0, 0.12), 0 4px 10px rgba(30, 64, 175, 0.08); border-radius: 12px; backdrop-filter: blur(10px);' }, grid: { - top: '10%', - left: '3%', - right: '4%', - bottom: '10%', + top: '8%', + left: '4%', + right: '3%', + bottom: '12%', containLabel: true }, xAxis: { @@ -1001,29 +1057,59 @@ const initProgressChart = () => { data: chartData.map(item => item.progressRange), axisLabel: { interval: 0, - rotate: 45, + rotate: 30, color: '#64748b', - fontSize: 12 + fontSize: 11, + fontWeight: 500 }, - axisLine: { lineStyle: { color: '#e2e8f0' } } + axisLine: { + lineStyle: { + color: '#cbd5e1', + width: 2 + } + }, + axisTick: { + show: false + } }, yAxis: { type: 'value', - name: '人数', - nameTextStyle: { color: '#64748b', padding: [0, 0, 0, 20] }, - splitLine: { lineStyle: { type: 'dashed', color: '#f1f5f9' } }, - axisLabel: { color: '#64748b' } + name: '学生人数', + nameTextStyle: { + color: '#475569', + padding: [0, 0, 10, 0], + fontSize: 13, + fontWeight: 600 + }, + splitLine: { + lineStyle: { + type: 'dashed', + color: '#e2e8f0', + width: 1 + } + }, + axisLabel: { + color: '#64748b', + fontSize: 12, + fontWeight: 500, + padding: [0, 8, 0, 0] + }, + axisLine: { show: false }, + axisTick: { show: false } }, series: [ { name: '学生人数', type: 'bar', - barWidth: '60%', - data: chartData.map(item => ({ + barWidth: '55%', + data: chartData.map((item, index) => ({ value: item.studentCount, itemStyle: { - color: progressColor(item.progressRange), - borderRadius: [4, 4, 0, 0] + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: progressColor(item.progressRange) }, + { offset: 1, color: progressColor(item.progressRange) + '99' } + ]), + borderRadius: [8, 8, 0, 0] } })), label: { @@ -1031,24 +1117,39 @@ const initProgressChart = () => { position: 'top', color: '#475569', fontSize: 12, - formatter: (params) => params.value > 0 ? params.value : '' - } + fontWeight: 600, + formatter: (params) => params.value > 0 ? `${params.value}人` : '', + offset: [0, -4] + }, + animationDelay: (idx) => idx * 80, + animationDuration: 1000, + animationEasing: 'cubicOut' } - ] + ], + animationDuration: 1200, + animationEasing: 'cubicOut' } progressChart.setOption(option) } // 监听 Tab 切换,重新渲染图表 -// watch(activeTab, (newVal) => { -// currentPage.value = 1 -// if (newVal === 'progress_dist') { -// nextTick(() => { -// initProgressChart() -// }) -// } -// }) +watch(activeTab, (newVal) => { + currentPage.value = 1 + if (newVal === 'progress_dist') { + nextTick(() => { + initProgressChart() + }) + } else if (newVal === 'accuracy_dist') { + nextTick(() => { + initAccuracyChart() + }) + } else if (newVal === 'online_stats') { + nextTick(() => { + initOnlineTrendChart() + }) + } +}) // 窗口大小变化时重绘图表 const handleResize = () => { @@ -1109,11 +1210,11 @@ const accuracyDistData = ref([ const levelColor = (range) => { const value = parseInt(range) - if (value >= 90) return '#00a870' - if (value >= 80) return '#0052d9' - if (value >= 60) return '#ed7b2f' - if (value >= 40) return '#f5a623' - return '#e34d59' + if (value >= 90) return '#10B981' + if (value >= 80) return '#3B82F6' + if (value >= 60) return '#F59E0B' + if (value >= 40) return '#F97316' + return '#EF4444' } const accuracyDistColumns = [ @@ -1150,18 +1251,34 @@ const initAccuracyChart = () => { const option = { tooltip: { trigger: 'axis', - axisPointer: { type: 'shadow' }, - formatter: '{b}
学生人数: {c}人', - backgroundColor: 'rgba(255, 255, 255, 0.95)', - borderColor: '#e2e8f0', + axisPointer: { + type: 'shadow', + shadowStyle: { + color: 'rgba(30, 64, 175, 0.08)' + } + }, + formatter: (params) => { + const data = params[0] + return `
+
${data.name}
+
+ + 学生人数: + ${data.value}人 +
+
` + }, + backgroundColor: 'rgba(255, 255, 255, 0.98)', + borderColor: 'rgba(30, 64, 175, 0.15)', + borderWidth: 1, textStyle: { color: '#1e293b' }, - extraCssText: 'box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); border-radius: 8px;' + extraCssText: 'box-shadow: 0 10px 25px rgba(0, 0, 0, 0.12), 0 4px 10px rgba(30, 64, 175, 0.08); border-radius: 12px; backdrop-filter: blur(10px);' }, grid: { - top: '10%', - left: '3%', - right: '4%', - bottom: '10%', + top: '8%', + left: '4%', + right: '3%', + bottom: '12%', containLabel: true }, xAxis: { @@ -1169,29 +1286,59 @@ const initAccuracyChart = () => { data: chartData.map(item => item.accuracyRange), axisLabel: { interval: 0, - rotate: 45, + rotate: 30, color: '#64748b', - fontSize: 12 + fontSize: 11, + fontWeight: 500 }, - axisLine: { lineStyle: { color: '#e2e8f0' } } + axisLine: { + lineStyle: { + color: '#cbd5e1', + width: 2 + } + }, + axisTick: { + show: false + } }, yAxis: { type: 'value', - name: '人数', - nameTextStyle: { color: '#64748b', padding: [0, 0, 0, 20] }, - splitLine: { lineStyle: { type: 'dashed', color: '#f1f5f9' } }, - axisLabel: { color: '#64748b' } + name: '学生人数', + nameTextStyle: { + color: '#475569', + padding: [0, 0, 10, 0], + fontSize: 13, + fontWeight: 600 + }, + splitLine: { + lineStyle: { + type: 'dashed', + color: '#e2e8f0', + width: 1 + } + }, + axisLabel: { + color: '#64748b', + fontSize: 12, + fontWeight: 500, + padding: [0, 8, 0, 0] + }, + axisLine: { show: false }, + axisTick: { show: false } }, series: [ { name: '学生人数', type: 'bar', - barWidth: '40%', + barWidth: '45%', data: chartData.map(item => ({ value: item.studentCount, itemStyle: { - color: levelColor(item.accuracyRange), - borderRadius: [4, 4, 0, 0] + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: levelColor(item.accuracyRange) }, + { offset: 1, color: levelColor(item.accuracyRange) + '99' } + ]), + borderRadius: [8, 8, 0, 0] } })), label: { @@ -1199,10 +1346,17 @@ const initAccuracyChart = () => { position: 'top', color: '#475569', fontSize: 12, - formatter: (params) => params.value > 0 ? params.value : '' - } + fontWeight: 600, + formatter: (params) => params.value > 0 ? `${params.value}人` : '', + offset: [0, -4] + }, + animationDelay: (idx) => idx * 80, + animationDuration: 1000, + animationEasing: 'cubicOut' } - ] + ], + animationDuration: 1200, + animationEasing: 'cubicOut' } accuracyChart.setOption(option) @@ -1373,22 +1527,58 @@ const initOnlineTrendChart = () => { const option = { tooltip: { trigger: 'axis', - axisPointer: { type: 'cross' }, - backgroundColor: 'rgba(255, 255, 255, 0.95)', - borderColor: '#e2e8f0', + axisPointer: { + type: 'cross', + crossStyle: { + color: '#94A3B8', + width: 1, + type: 'dashed' + }, + lineStyle: { + color: '#94A3B8', + width: 2, + type: 'dashed' + } + }, + formatter: (params) => { + return `
+
${params[0].axisValue}
+
+ + 在线用户数: + ${params[0].value}人 +
+
+ + 答题数量: + ${params[1].value}次 +
+
` + }, + backgroundColor: 'rgba(255, 255, 255, 0.98)', + borderColor: 'rgba(30, 64, 175, 0.15)', + borderWidth: 1, textStyle: { color: '#1e293b' }, - extraCssText: 'box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); border-radius: 8px;' + extraCssText: 'box-shadow: 0 10px 25px rgba(0, 0, 0, 0.12), 0 4px 10px rgba(30, 64, 175, 0.08); border-radius: 12px; backdrop-filter: blur(10px);' }, legend: { data: ['在线用户数', '答题数量'], - top: 0, - textStyle: { color: '#475569' } + top: 8, + itemWidth: 18, + itemHeight: 10, + itemGap: 24, + textStyle: { + color: '#475569', + fontSize: 13, + fontWeight: 500 + }, + icon: 'roundRect' }, grid: { - top: '15%', - left: '3%', + top: '16%', + left: '4%', right: '4%', - bottom: '10%', + bottom: '12%', containLabel: true }, xAxis: { @@ -1397,60 +1587,132 @@ const initOnlineTrendChart = () => { data: trendData.time, axisLabel: { color: '#64748b', - fontSize: 12, - interval: onlineTimeDimension.value === 'minute' ? 5 : 0, // 10分钟维度下每隔5个点(即1小时)显示一个标签 - rotate: onlineTimeDimension.value === 'day' ? 45 : 0 // 按天显示时倾斜标签以防重叠 + fontSize: 11, + fontWeight: 500, + interval: onlineTimeDimension.value === 'minute' ? 5 : 0, + rotate: onlineTimeDimension.value === 'day' ? 30 : 0, + padding: [8, 0, 0, 0] }, - axisLine: { lineStyle: { color: '#e2e8f0' } } + axisLine: { + lineStyle: { + color: '#cbd5e1', + width: 2 + } + }, + axisTick: { show: false } }, yAxis: [ { type: 'value', name: '在线用户数', position: 'left', - splitLine: { lineStyle: { type: 'dashed', color: '#f1f5f9' } }, - axisLabel: { color: '#64748b' }, - nameTextStyle: { color: '#64748b', padding: [0, 0, 0, 20] } + splitLine: { + lineStyle: { + type: 'dashed', + color: '#e2e8f0', + width: 1 + } + }, + axisLabel: { + color: '#64748b', + fontSize: 12, + fontWeight: 500, + padding: [0, 8, 0, 0] + }, + nameTextStyle: { + color: '#475569', + padding: [0, 0, 10, 0], + fontSize: 13, + fontWeight: 600 + }, + axisLine: { show: false }, + axisTick: { show: false } }, { type: 'value', name: '答题数量', position: 'right', splitLine: { show: false }, - axisLabel: { color: '#64748b' }, - nameTextStyle: { color: '#64748b', padding: [0, 20, 0, 0] } + axisLabel: { + color: '#64748b', + fontSize: 12, + fontWeight: 500, + padding: [0, 0, 0, 8] + }, + nameTextStyle: { + color: '#475569', + padding: [0, 20, 10, 0], + fontSize: 13, + fontWeight: 600 + }, + axisLine: { show: false }, + axisTick: { show: false } } ], series: [ { name: '在线用户数', type: 'line', - smooth: true, + smooth: 0.4, yAxisIndex: 0, - itemStyle: { color: '#0052d9' }, + symbol: 'circle', + symbolSize: 6, + itemStyle: { + color: '#1E40AF', + borderWidth: 2, + borderColor: '#fff' + }, + lineStyle: { + width: 3, + shadowColor: 'rgba(30, 64, 175, 0.3)', + shadowBlur: 10, + shadowOffsetY: 4 + }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ - { offset: 0, color: 'rgba(0, 82, 217, 0.3)' }, - { offset: 1, color: 'rgba(0, 82, 217, 0.05)' } + { offset: 0, color: 'rgba(30, 64, 175, 0.35)' }, + { offset: 0.5, color: 'rgba(30, 64, 175, 0.15)' }, + { offset: 1, color: 'rgba(30, 64, 175, 0.02)' } ]) }, - data: trendData.onlineCount + data: trendData.onlineCount, + animationDelay: 0, + animationDuration: 1200, + animationEasing: 'cubicOut' }, { name: '答题数量', type: 'line', - smooth: true, + smooth: 0.4, yAxisIndex: 1, - itemStyle: { color: '#00a870' }, + symbol: 'circle', + symbolSize: 6, + itemStyle: { + color: '#10B981', + borderWidth: 2, + borderColor: '#fff' + }, + lineStyle: { + width: 3, + shadowColor: 'rgba(16, 185, 129, 0.3)', + shadowBlur: 10, + shadowOffsetY: 4 + }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ - { offset: 0, color: 'rgba(0, 168, 112, 0.3)' }, - { offset: 1, color: 'rgba(0, 168, 112, 0.05)' } + { offset: 0, color: 'rgba(16, 185, 129, 0.35)' }, + { offset: 0.5, color: 'rgba(16, 185, 129, 0.15)' }, + { offset: 1, color: 'rgba(16, 185, 129, 0.02)' } ]) }, - data: trendData.answerCount + data: trendData.answerCount, + animationDelay: 200, + animationDuration: 1200, + animationEasing: 'cubicOut' } - ] + ], + animationDuration: 1400, + animationEasing: 'cubicOut' } onlineTrendChart.setOption(option) @@ -1465,6 +1727,24 @@ import * as XLSX from 'xlsx' const exportDialogVisible = ref(false) const exportType = ref('') +const exporting = ref(false) + +const exportDialogTitle = computed(() => { + switch (exportType.value) { + case 'overview': + return '总体学情统计' + case 'accuracy_rank': + return '答题正确率排名' + case 'progress_dist': + return '答题进度分布' + case 'accuracy_dist': + return '正确率分布' + case 'online_stats': + return '学生在线统计' + default: + return '数据' + } +}) const exportFilters = ref({ cloud: '', @@ -1506,97 +1786,94 @@ const handleExport = (type) => { exportDialogVisible.value = true } -const confirmExport = () => { - exportDialogVisible.value = false +const confirmExport = async () => { + exporting.value = true - let dataToExport = [] - let columns = [] - let fileName = '' + try { + let dataToExport = [] + let columns = [] + let fileName = '' - // 在实际应用中,这里应该根据 exportFilters 的值重新请求数据 - // 由于当前是静态数据展示,我们直接使用现有的数据作为导出数据 + switch (exportType.value) { + case 'overview': + dataToExport = overviewData.value + columns = overviewColumns + fileName = '总体学情统计分析表' + break + case 'accuracy_rank': + dataToExport = accuracyRankData.value + columns = accuracyRankColumns + fileName = '学生答题正确率排名表' + break + case 'progress_dist': + dataToExport = progressDistData.value + columns = progressDistColumns + fileName = '学生答题进度分布表' + break + case 'accuracy_dist': + dataToExport = accuracyDistData.value + columns = accuracyDistColumns + fileName = '学生答题正确率分布表' + break + case 'online_stats': + const originalOnlineSingleDate = onlineSingleDate.value + const originalOnlineDateRange = onlineDateRange.value + + if (onlineTimeDimension.value === 'minute' || onlineTimeDimension.value === 'hour') { + onlineSingleDate.value = exportFilters.value.singleDate + } else { + onlineDateRange.value = exportFilters.value.dateRange + } + + const trendData = generateTrendData(onlineTimeDimension.value) + + onlineSingleDate.value = originalOnlineSingleDate + onlineDateRange.value = originalOnlineDateRange + + dataToExport = trendData.time.map((t, index) => ({ + time: t, + onlineCount: trendData.onlineCount[index], + answerCount: trendData.answerCount[index] + })) + columns = [ + { colKey: 'time', title: '时间' }, + { colKey: 'onlineCount', title: '在线用户数' }, + { colKey: 'answerCount', title: '答题数量' } + ] + fileName = '学生在线与答题趋势' + break + } - switch (exportType.value) { - case 'overview': - dataToExport = overviewData.value - columns = overviewColumns - fileName = '总体学情统计分析表' - break - case 'accuracy_rank': - dataToExport = accuracyRankData.value - columns = accuracyRankColumns - fileName = '学生答题正确率排名表' - break - case 'progress_dist': - dataToExport = progressDistData.value - columns = progressDistColumns - fileName = '学生答题进度分布表' - break - case 'accuracy_dist': - dataToExport = accuracyDistData.value - columns = accuracyDistColumns - fileName = '学生答题正确率分布表' - break - case 'online_stats': - // 模拟根据导出对话框中的筛选条件生成数据 - // 实际应用中应该传递 exportFilters 重新请求数据 - // 这里为了保持演示,临时修改一下组件内的状态来生成对应数据,生成后再恢复 - const originalOnlineSingleDate = onlineSingleDate.value - const originalOnlineDateRange = onlineDateRange.value - - if (onlineTimeDimension.value === 'minute' || onlineTimeDimension.value === 'hour') { - onlineSingleDate.value = exportFilters.value.singleDate - } else { - onlineDateRange.value = exportFilters.value.dateRange - } - - const trendData = generateTrendData(onlineTimeDimension.value) - - // 恢复原始状态 - onlineSingleDate.value = originalOnlineSingleDate - onlineDateRange.value = originalOnlineDateRange - - dataToExport = trendData.time.map((t, index) => ({ - time: t, - onlineCount: trendData.onlineCount[index], - answerCount: trendData.answerCount[index] - })) - columns = [ - { colKey: 'time', title: '时间' }, - { colKey: 'onlineCount', title: '在线用户数' }, - { colKey: 'answerCount', title: '答题数量' } - ] - fileName = '学生在线与答题趋势' - break - } - - // 提取表头 - const headers = columns.map(col => col.title) - // 提取数据 - const rows = dataToExport.map(row => { - return columns.map(col => { - return row[col.colKey] + const headers = columns.map(col => col.title) + const rows = dataToExport.map(row => { + return columns.map(col => { + return row[col.colKey] + }) }) - }) - // 组装工作表数据 - const worksheetData = [headers, ...rows] - const worksheet = XLSX.utils.aoa_to_sheet(worksheetData) - const workbook = XLSX.utils.book_new() - XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1') + const worksheetData = [headers, ...rows] + const worksheet = XLSX.utils.aoa_to_sheet(worksheetData) + const workbook = XLSX.utils.book_new() + XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1') - // 获取筛选条件的标签用于生成文件名 - const expCloudLabel = cloudList.find(item => item.value === exportFilters.value.cloud)?.label || '' - const expSchoolLabel = schoolList.find(item => item.value === exportFilters.value.school)?.label || '' - const expGradeLabel = gradeList.find(item => item.value === exportFilters.value.grade)?.label || '' - const expClassLabel = classList.find(item => item.value === exportFilters.value.class)?.label || '' + const expCloudLabel = cloudList.find(item => item.value === exportFilters.value.cloud)?.label || '' + const expSchoolLabel = schoolList.find(item => item.value === exportFilters.value.school)?.label || '' + const expGradeLabel = gradeList.find(item => item.value === exportFilters.value.grade)?.label || '' + const expClassLabel = classList.find(item => item.value === exportFilters.value.class)?.label || '' - // 构造文件名,包含筛选条件 - const filterStr = [expCloudLabel, expSchoolLabel, expGradeLabel, expClassLabel].filter(Boolean).join('_') - const finalFileName = `${fileName}${filterStr ? '_' + filterStr : ''}.xlsx` + const filterStr = [expCloudLabel, expSchoolLabel, expGradeLabel, expClassLabel].filter(Boolean).join('_') + const finalFileName = `${fileName}${filterStr ? '_' + filterStr : ''}.xlsx` - XLSX.writeFile(workbook, finalFileName) - MessagePlugin.success('导出成功') + await new Promise(resolve => setTimeout(resolve, 500)) + + XLSX.writeFile(workbook, finalFileName) + exportDialogVisible.value = false + MessagePlugin.success('导出成功') + } catch (error) { + MessagePlugin.error('导出失败,请重试') + } finally { + exporting.value = false + } } // 级联重置:切换学校时清空年级和班级 @@ -1860,111 +2137,147 @@ watch(activeTab, (newVal) => {