手把手制作Vue3+Flask全栈项目 全栈开发之路实战篇 问卷网站(四)实现用户视图
全栈开发一条龙——前端篇
第一篇:框架确定、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
<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的顺序取出数据了~
总结
本次,我们实现了用户视图的编写,将用户的答卷转化为字符串存进数据库,下一次,我们制作数据处理的页面。
更多推荐
所有评论(0)