(九)学生写作画像可视化
在上次报告中提到的无法正确识别登录状态的问题已经解决,现在调用后端api时可以正确load_user并得到登录状态。
登录状态问题解决后,本次主要实现的是学生写作画像的数据可视化,学生可以登陆后查看自己之前的作文列表与历史各维度得分,获得得分雷达图,进行数据可视化。
一、学生写作中心后端
为了实现学生写作中心,主要是后端新增student_dashboard_routes.py。该文件定义了获取学生文章列表,学生作文分数,老师反馈等api供前端调用。
在get_student_essays中,通过获取currentuserid,进而使用models.py中已经定义的get_writing_history方法来查询每个studentid下所有的essay,并且在essay_list中读取每个essay的id来获取分数并计算平均分。
@student_dashboard.route('/student/essays', methods=['GET'])
@login_required
def get_student_essays():"""获取学生的所有作文列表"""try:user_model = UserModel(mysql)essays = user_model.get_writing_history(current_user.id)essay_list = []for essay in essays:essay_id = int(essay['essay_id']) # 确保转为整数print(f"当前处理作文ID: {essay_id}") # 使用f-string # 获取作文评分profile = user_model.get_writing_profile(current_user.id,essay_id)# 获取教师评语feedback = user_model.get_teacher_feedback(current_user.id)essay_data = {'id': essay['essay_id'],'title': essay.get('title', ''),'createTime': essay['created_at'].isoformat(),'updateTime': essay['created_at'].isoformat(),'score': None,'scores': None,'comments': None}# 添加评分信息if profile:essay_data['score'] = round((profile['grammar_score'] + profile['idea_score'] + profile['structure_score'] + profile['rhetoric_score'] + profile['emotion_score'] + profile['innovation_score']) / 6, 1)essay_data['scores'] = {'grammar': round(profile['grammar_score'], 1),'idea': round(profile['idea_score'], 1),'structure': round(profile['structure_score'], 1),'rhetoric': round(profile['rhetoric_score'], 1),'emotion': round(profile['emotion_score'], 1),'innovation': round(profile['innovation_score'], 1)}# 添加教师评语if feedback:essay_data['comments'] = feedback[0]['feedback_content'] if feedback else Noneessay_list.append(essay_data)return jsonify({'success': True,'data': essay_list})except Exception as e:return jsonify({'success': False,'error': str(e)}), 500
而在调用get_student_essays之后,便可以使用get_essay_detail,通过essay_id来获取更为详细的作文内容,这些返回给前端,前端将这些数据生成雷达图进行可视化。
@student_dashboard.route('/student/essay/<int:essay_id>', methods=['GET'])
@login_required
def get_essay_detail(essay_id):"""获取单篇作文的详细信息"""try:user_model = UserModel(mysql)# 获取作文内容cursor = mysql.connection.cursor(MySQLdb.cursors.DictCursor)cursor.execute('SELECT * FROM essays WHERE id = %s AND student_id = %s',(essay_id, current_user.id))essay = cursor.fetchone()cursor.close()if not essay:return jsonify({'success': False,'error': '作文不存在或无权访问'}), 404# 获取作文评分profile = user_model.get_writing_profile(current_user.id,essay_id)# 获取教师评语feedback = user_model.get_teacher_feedback(current_user.id)# 获取写作风格特征style = user_model.get_style_features(current_user.id)essay_detail = {'id': essay['id'],'title': essay['title'],'content': essay['content'],'createTime': essay['submission_date'].isoformat(),'updateTime': essay['submission_date'].isoformat(),'score': None,'scores': None,'comments': None,'styleFeatures': None}# 添加评分信息if profile:essay_detail['score'] = round((profile['grammar_score'] + profile['idea_score'] + profile['structure_score'] + profile['rhetoric_score'] + profile['emotion_score'] + profile['innovation_score']) / 6, 1)essay_detail['scores'] = {'grammar': round(profile['grammar_score'], 1),'idea': round(profile['idea_score'], 1),'structure': round(profile['structure_score'], 1),'rhetoric': round(profile['rhetoric_score'], 1),'emotion': round(profile['emotion_score'], 1),'innovation': round(profile['innovation_score'], 1)}# 添加教师评语if feedback:essay_detail['comments'] = feedback[0]['feedback_content'] if feedback else None# 添加写作风格特征if style:essay_detail['styleFeatures'] = {'vocabularyDiversity': style[0]['vocabulary_diversity'],'sentenceComplexity': style[0]['sentence_complexity'],'paragraphCoherence': style[0]['paragraph_coherence'],'argumentPatterns': style[0]['argument_patterns'],'wordChoiceFeatures': style[0]['word_choice_features'],'styleKeywords': style[0]['style_keywords']}return jsonify({'success': True,'data': essay_detail})except Exception as e:return jsonify({'success': False,'error': str(e)}), 500
这样就将之前所用的loginManager结合起来,实现用户登录查询。
二、学生写作中心前端
因为后端整个登录逻辑的更改,为了与后端登录认证相兼容,前端的登陆方法也进行一些改变。
通过在原始前端方法中加入withCredentials: true,使得浏览器能够保存并使用初始登录的session,进而确保之后每次调用api都会使用相同的session,这也也就使得后端能认证登录的状态。
const handleLogin = async () => {try {const response = await axios.post('http://localhost:5000/api/login', form.value, {withCredentials: true // ✅ 关键设置});if (response.data.status === 'success') {router.push('/student-dashboard');}} catch (err) {error.value = err.response?.data?.message || '登录失败';}
};
前端为了获取文章,调用/api/student/essays,后端成功调用即可返回
async fetchEssays() {this.loading = true;this.error = null;try {const response = await axios.get('/api/student/essays', {withCredentials: true,headers: {'Accept': 'application/json','Content-Type': 'application/json'}});if (response.data.success) {this.essays = response.data.data;if (this.essays.length > 0) {this.selectEssay(this.essays[0]);}}} catch (err) {console.error('获取作文列表失败:', err);this.error = '获取作文列表失败,请稍后重试';} finally {this.loading = false;}},
获取文章数据后可使用getDimensionName,initRadarChart初始化可视化图表
getDimensionName(dimension) {const dimensionMap = {grammar: '语法',idea: '内容',structure: '结构',rhetoric: '修辞',emotion: '情感',innovation: '创新'};return dimensionMap[dimension] || dimension;},initRadarChart() {if (!this.selectedEssay || !this.selectedEssay.scores) return;if (this.chart) {this.chart.dispose();}const chartDom = this.$refs.radarChart;this.chart = echarts.init(chartDom);const dimensions = Object.keys(this.selectedEssay.scores);const scores = Object.values(this.selectedEssay.scores);const option = {radar: {indicator: dimensions.map(dim => ({name: this.getDimensionName(dim),max: 100})),splitNumber: 4,axisName: {color: '#666'}},series: [{type: 'radar',data: [{value: scores,name: '能力维度',areaStyle: {color: 'rgba(74, 144, 226, 0.3)'},lineStyle: {color: '#4a90e2'},itemStyle: {color: '#4a90e2'}}]}]};this.chart.setOption(option);}},