赞
踩
全栈开发一条龙——前端篇
第一篇:框架确定、ide设置与项目创建
第二篇:介绍项目文件意义、组件结构与导入以及setup的引入。
第三篇:setup语法,设置响应式数据。
第四篇:数据绑定、计算属性和watch监视
第五篇 : 组件间通信及知识补充
第六篇:生命周期和自定义hooks
第七篇:路由
第八篇:传参
第九篇:插槽,常用api和全局api。
全栈开发一条龙——全栈篇
第一篇:初识Flask&MySQL实现前后端通信
第二篇: sql操作、发送http请求和邮件发送
第三篇:全栈实现发送验证码注册账号
第四篇:图片验证码及知识补充
全栈开发一条龙——实战篇
第一篇:项目建立与login页面
第二篇:建立管理员后台和添加题目功能实现
第三篇:完善管理员后台
接下来我们来制作用户视图,显示题目在页面上,并提供提交的按钮。其中,题目根据用户的行为实时获取。
我们先为我们的用户视图添加上路由设置
在index.ts中加入
{
path:'/user_home',
name:'user_home',
component:user_home
},
接下来,我们在login中加入验证逻辑,识别如果账号是用户账号,就自动跳转用户页面。
与之前的类似,我们在login里同一个判断中加入一个新的判断逻辑
async function login() { try{ let result=await axios.get(urlStore.urls.login,{params:{ account:account.value, password:password.value }}) window.alert(result.data.msg) if(result.data.errcode == 0) { localStorage.setItem("token",result.data.token) router.replace setTimeout(()=>{router.push({path:"/home"})},2500) } if (result.data.errcode == -1) { localStorage.setItem("token",result.data.token) router.replace setTimeout(()=>{router.push({path:"user_home"})},2500) } }catch(error){alert(error)} };
<template> <div class="container"> <div class="header"> <h1>谢谢谢谢学习力调查问卷表</h1> <button v-if="showLogout" @click="logout" class="logout-button">切换题目编辑模式</button> </div> <div v-if="loading" class="loading">Loading...</div> <div v-else class="questionnaire"> <transition-group name="fade" tag="div" class="r"> <div v-for="question in filteredQuestions" :key="question.id" class="question-item"> <QuestionComponent :question="question" @select="handleSelect" /> </div> </transition-group> <button @click="submitQuestionnaire" class="submit-button">Submit</button> </div> </div> </template>
我们先来写template,这里我们展示标题,然后在标题右边加上一个切换页面的按钮,只有管理员可以看到这个回到编辑页面的按钮。
接下来,我们循环取出question中的内容,并设置出现逻辑(展示filteredquestion中的题目)
最后,写一个提交按钮。
那我们接下来要用脚本实现以上的模板
我们需要先获取到问题列表
const fetchQuestions = async () => { try { const result = await axios.post(urlStore.urls.sqlex); const data = result.data.data; questions.value = data.map(item => ({ id: item.id, question_text: item.questions, choice_a: item.choice_a, score_a: item.point_a, choice_b: item.point_b, score_b: item.point_b, choice_c: item.choice_c, score_c: item.point_c, choice_d: item.choice_d, score_d: item.point_d, stage: item.stage })); // 统计每个stage的问题数量 stageQuestionCounts.value = [0, 0, 0, 0]; questions.value.forEach(question => { if (question.stage >= 1 && question.stage <= 4) { stageQuestionCounts.value[question.stage - 1]++; } }); } catch (error) { console.error("Error fetching questions:", error); } finally { loading.value = false; } };
这里不涉及展示逻辑,只是让我们的前端拿到问题数据。
接下来,我们使用一个更新阶段函数,来判断用户的答题情况,根据用户的得分来更新我们的问题展示列表
const updateStage = () => { let stageScores = [0, 0, 0, 0]; // Scores for stages 1 to 4 for (const questionId in userAnswers.value) { const answer = userAnswers.value[questionId]; const question = questions.value.find(q => q.id === questionId); stageScores[question.stage - 1] += question[`score_${answer}`]; } const stageThresholds = stageQuestionCounts.value.map(count => Math.ceil(count * 1.5)); if (stageScores[0] >= stageThresholds[0] && currentStage.value < 2) { currentStage.value = 2; } if (stageScores[1] >= stageThresholds[1] && currentStage.value < 3) { currentStage.value = 3; } if (stageScores[2] >= stageThresholds[2] && currentStage.value < 4) { currentStage.value = 4; } };
const filteredQuestions = computed(() => {
return questions.value.filter(question => question.stage <= currentStage.value);
});
最后,我们实现submit按钮,我们先要想想用什么格式提交我们的问卷答案,我这里使用了一个字符串,比如ABBDC,然后在后端解析、入库。同时,为了方便我们统计达到各个阶段的人数,如果用户未能做到新阶段,用N占位,这样字符串长度总等于问题数。
const submitQuestionnaire = async () => { let token = localStorage.getItem("token"); if (token) { try { let answerString = generateAnswerString(); if (answerString) { let result = await axios.get('http://127.0.0.1:5000/data_ex/', { params: { answer: answerString, token: token } }); window.alert(result.data.msg); } } catch (error) { alert(error); } } else { alert("登录过期"); } };
const generateAnswerString = () => { let answerArray = []; for (let i = 1; i <= 4; i++) { let stageQuestions = questions.value.filter(q => q.stage === i); for (let [index, question] of stageQuestions.entries()) { if (question.stage <= currentStage.value) { if (userAnswers.value[question.id]) { answerArray.push(userAnswers.value[question.id].toUpperCase()); } else { window.alert(`请回答第 ${index + 1} 题`); return null; } } else { answerArray.push('N'); } } } return answerArray.join(''); };
我们在挂载组件的时候最好验证一下用户身份
// Verify token function const verifyToken = async () => { let token = localStorage.getItem("token"); if (token) { try { // 发送token到后端验证 const response = await axios.post(urlStore.urls.login, { token:token }); if (response.data.errcode == 0) { showLogout.value = true; } else { showLogout.value = false; } } catch (error) { console.error("Error verifying token:", error); showLogout.value = false; } } else { showLogout.value = false; } };
防止由于有人知道路由,不需要token直接访问。
完整代码如下
<template> <div class="container"> <div class="header"> <h1>谢谢谢谢学习力调查问卷表</h1> <button v-if="showLogout" @click="logout" class="logout-button">切换题目编辑模式</button> </div> <div v-if="loading" class="loading">Loading...</div> <div v-else class="questionnaire"> <transition-group name="fade" tag="div" class="r"> <div v-for="(question, index) in filteredQuestions" :key="question.id" class="question-item"> <QuestionComponent :question="question" :index="index + 1" @select="handleSelect" /> </div> </transition-group> <button @click="submitQuestionnaire" class="submit-button">Submit</button> </div> </div> </template> <script setup> import { ref, computed, onMounted } from 'vue'; import axios from 'axios'; import QuestionComponent from '@/components/QuestionComponent.vue'; import { useUrlStore } from '@/store/urlStore'; import { useRouter } from 'vue-router'; const router = useRouter(); const loading = ref(true); const questions = ref([]); const userAnswers = ref({}); const urlStore = useUrlStore(); const currentStage = ref(1); const showLogout = ref(false); const stageQuestionCounts = ref([0, 0, 0, 0]); const fetchQuestions = async () => { try { const result = await axios.post(urlStore.urls.sqlex); const data = result.data.data; questions.value = data.map(item => ({ id: item.id, question_text: item.questions, choice_a: item.choice_a, score_a: item.point_a, choice_b: item.choice_b, score_b: item.point_b, choice_c: item.choice_c, score_c: item.point_c, choice_d: item.choice_d, score_d: item.point_d, stage: item.stage })); stageQuestionCounts.value = [0, 0, 0, 0]; questions.value.forEach(question => { if (question.stage >= 1 && question.stage <= 4) { stageQuestionCounts.value[question.stage - 1]++; } }); } catch (error) { console.error("Error fetching questions:", error); } finally { loading.value = false; } }; const handleSelect = (questionId, choice) => { userAnswers.value[questionId] = choice; updateStage(); }; const updateStage = () => { let stageScores = [0, 0, 0, 0]; for (const questionId in userAnswers.value) { const answer = userAnswers.value[questionId]; const question = questions.value.find(q => q.id === questionId); stageScores[question.stage - 1] += question[`score_${answer}`]; } const stageThresholds = stageQuestionCounts.value.map(count => Math.ceil(count * 1.5)); if (stageScores[0] >= stageThresholds[0] && currentStage.value < 2) { currentStage.value = 2; } if (stageScores[1] >= stageThresholds[1] && currentStage.value < 3) { currentStage.value = 3; } if (stageScores[2] >= stageThresholds[2] && currentStage.value < 4) { currentStage.value = 4; } }; const filteredQuestions = computed(() => { return questions.value.filter(question => question.stage <= currentStage.value); }); const submitQuestionnaire = async () => { let token = localStorage.getItem("token"); if (token) { try { let answerString = generateAnswerString(); if (answerString) { let result = await axios.get('http://127.0.0.1:5000/data_ex/', { params: { answer: answerString, token: token } }); window.alert(result.data.msg); } } catch (error) { alert(error); } } else { alert("登录过期"); } }; const generateAnswerString = () => { let answerArray = []; for (let i = 1; i <= 4; i++) { let stageQuestions = questions.value.filter(q => q.stage === i); for (let [index, question] of stageQuestions.entries()) { if (question.stage <= currentStage.value) { if (userAnswers.value[question.id]) { answerArray.push(userAnswers.value[question.id].toUpperCase()); } else { window.alert(`请回答第 ${index + 1} 题`); return null; } } else { answerArray.push('N'); } } } return answerArray.join(''); }; // Logout function to clear token const logout = () => { alert("切换成功,一秒后跳转编辑页面"); setTimeout(() => { router.push({ path: "/home" }) }, 1000); }; // Verify token function const verifyToken = async () => { let token = localStorage.getItem("token"); if (token) { try { const response = await axios.post(urlStore.urls.login, { token: token }); if (response.data.errcode == 0) { showLogout.value = true; } else { showLogout.value = false; } } catch (error) { console.error("Error verifying token:", error); showLogout.value = false; } } else { showLogout.value = false; } }; onMounted(() => { fetchQuestions(); verifyToken(); }); </script> <style scoped> .container { display: flex; flex-direction: column; align-items: center; width: 45vw; padding: 20px; box-sizing: border-box; margin-left: 17vw; margin-right: 15vw; } .header { width: 100%; display: flex; justify-content: center; align-items: center; position: relative; } h1 { font-size: 2.5em; margin: 30px 0; color: #333333; font-weight: bold; text-transform: uppercase; letter-spacing: 1px; text-align: center; flex-grow: 1; } .logout-button { position: absolute; top: 0; right: 0; background-color: #5a88d2; color: white; border: none; width: 120px; height: 30px; border-radius: 4px; cursor: pointer; font-size: 12px; display: flex; justify-content: center; align-items: center; } .logout-button:hover { background-color: #243f89; } .loading { color: #999; } .questionnaire { display: flex; flex-direction: column; align-items: center; background: #f7f6f2; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); width: 100%; } .r { display: flex; flex-direction: column; align-items: center; } .question-item { width: 75%; margin-bottom: 20px; } .submit-button { display: block; width: 100%; max-width: 600px; padding: 10px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } .submit-button:hover { background-color: #0056b3; } .fade-enter-active, .fade-leave-active { transition: opacity 0.5s; } .fade-enter-from, .fade-leave-to { opacity: 0; } </style>
我们同理先写好操作数据库的工具文件
from set_app import app from data_set import db from flask import current_app from sqlalchemy import text with app.app_context(): class analysis_ex_object(db.Model): __tablename__ = 'data_analysis' id = db.Column(db.Integer,primary_key = True,autoincrement=True) answer = db.Column(db.String(300)) class answer_ex(): def add(self,answer): with app.app_context(): answer_add = analysis_ex_object() answer_add.answer = answer try: db.session.add(answer_add) db.session.commit() db.session.close() return "提交成功" except: return "提交失败"
然后建立蓝图文件,提供dataex接口,来进行数据入库
from flask import Blueprint, jsonify, request from flask.views import MethodView from data_analysis_tools import answer_ex from token_ex import jwt_safe dataex = Blueprint("data_ex", __name__) class data_ex_(MethodView): def get(self): token =request.args.get("token") token = jwt_safe(token) res = token.decode() if res['account'] != "123456": return jsonify( {"errcode":1,"msg":"无权限"} ) answer =request.args.get("answer") print("\n\n\n",answer,"\n\n\n") try: temp = answer_ex() res=temp.add(answer=answer) return jsonify( {"errcode":0,"msg":res} ) except: return jsonify( {"errcode":1,"msg":res} ) def post(self): pass dataex.add_url_rule("/data_ex/", view_func=data_ex_.as_view("data_ex"))
我们在显示题目的时候,由于题目是乱序放在数据库里的,而我们又需要按照stage来提供给用户,所以可以在获取数据的时候在后端加一个排序算法
def search(self):
with app.app_context():
raw_list = db.session.execute( text("select * from questions") ).fetchall()
list = list_row2list_dic(raw_list)
list = sorted(list,key=lambda i:i['stage'])
return list
这样就可以按照stage的顺序取出数据了~
本次,我们实现了用户视图的编写,将用户的答卷转化为字符串存进数据库,下一次,我们制作数据处理的页面。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。