手把手制作Vue3+Flask全栈项目 全栈开发之路实战篇 问卷网站(三)实现管理员后台
全栈开发一条龙——前端篇
第一篇:框架确定、ide设置与项目创建
第二篇:介绍项目文件意义、组件结构与导入以及setup的引入。
第三篇:setup语法,设置响应式数据。
第四篇:数据绑定、计算属性和watch监视
第五篇 : 组件间通信及知识补充
第六篇:生命周期和自定义hooks
第七篇:路由
第八篇:传参
第九篇:插槽,常用api和全局api。
全栈开发一条龙——全栈篇
第一篇:初识Flask&MySQL实现前后端通信
第二篇: sql操作、发送http请求和邮件发送
第三篇:全栈实现发送验证码注册账号
第四篇:图片验证码及知识补充
全栈开发一条龙——实战篇
第一篇:项目建立与login页面
第二篇:建立管理员后台和添加题目功能实现
我们上一章已经将管理员后台大刀阔斧的写完了框架,添加了增加的题目,本章,我们将进一步丰富细节,实现增删改查以及验证管理员身份等等,完善我们的管理员后台。
一、题目显示的前后端串联
后端
我们之前已经把操作数据库的工具写好了,所以现在只需要给我们的蓝图中加入显示的内容就好了。
def post(self):
temp = sql_ex.sql_ex()
temp = temp.search()
d2js={"data":temp}
print(d2js)
return jsonify(d2js)
相当简单,建立一个post方法,接收到请求之后从数据库中取出数据,然后传递给前端。需要说明的是,我们传给前端的内容都需要是jsonify的dic数据。我们查询数据库查出来是sql自定义的数据的一个列表,然后我们用之前讲过的(已经写在上次的sqlex中了)
def list_row2list_dic(list_row):
dic_temp = {}
list_dic = []
for x in list_row:
listda = []
listidx= []
for dx in x:
listda.append(dx)
xx = x._key_to_index
for idx in xx:
listidx.append(idx)
dic_temp=dict(zip(listidx,listda))
list_dic.append(dic_temp)
return list_dic
这一段代码,来实现数据库自定义列表传化为python自带的dic内容,然后我们在蓝图中,在将dic内容打包,放在data里,这样我们前端使用.data就可以调用到了。
前端
我们之前使用的静态数据,现在我们先从后端获取数据,然后将数据赋值给我们前端的question列表,最后我们展示这些题目。
async function fetchQuestions(){
try {
const result = await axios.post('http://127.0.0.1:5000/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
}));
} catch (error) {
console.error(error);
}
};
完整代码如下
<template>
<div>
<h2>所有题目</h2>
<ul>
<li
v-for="question in questions"
:key="question.id"
@click="selectQuestion(question)"
:class="{ selected: question.id === selectedQuestionId }"
>
<h3>ID: {{ question.id }} - {{ question.question_text }}</h3>
<p>A. {{ question.choice_a }} ({{ question.score_a }} 分)</p>
<p>B. {{ question.choice_b }} ({{ question.score_b }} 分)</p>
<p>C. {{ question.choice_c }} ({{ question.score_c }} 分)</p>
<p>D. {{ question.choice_d }} ({{ question.score_d }} 分)</p>
<!-- <button @click.stop="deleteQuestion(question.id)">删除</button> -->
</li>
</ul>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { defineProps, defineEmits } from 'vue';
import axios from 'axios';
const props = defineProps({
selectedQuestionId: {
type: Number,
default: null
}
});
const emit = defineEmits(['selectQuestion', 'refreshQuestions']);
const questions = ref([]);
async function fetchQuestions(){
try {
const result = await axios.post('http://127.0.0.1:5000/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
}));
} catch (error) {
console.error(error);
}
// 暂时使用静态数据
// questions.value = [
// {
// id: 1,
// question_text: '题目 1',
// choice_a: '选项 A1',
// score_a: 1,
// choice_b: '选项 B1',
// score_b: 2,
// choice_c: '选项 C1',
// score_c: 3,
// choice_d: '选项 D1',
// score_d: 4
// },
// {
// id: 2,
// question_text: '题目 2',
// choice_a: '选项 A2',
// score_a: 1,
// choice_b: '选项 B2',
// score_b: 2,
// choice_c: '选项 C2',
// score_c: 3,
// choice_d: '选项 D2',
// score_d: 4
// },
// {
// id: 3,
// question_text: '题目 3',
// choice_a: '选项 A3',
// score_a: 1,
// choice_b: '选项 B3',
// score_b: 2,
// choice_c: '选项 C3',
// score_c: 3,
// choice_d: '选项 D3',
// score_d: 4
// }
// ];
};
const selectQuestion = (question) => {
emit('selectQuestion', question);
};
const deleteQuestion = (questionId) => {
// 这里可以添加发送请求到后端的代码
// axios.delete(`/api/questions/${questionId}`)
// .then(response => {
// console.log(response.data);
// fetchQuestions();
// emit('refreshQuestions');
// });
// 暂时使用静态数据
questions.value = questions.value.filter(question => question.id !== questionId);
emit('refreshQuestions');
};
onMounted(fetchQuestions);
</script>
<style scoped>
h2 {
color: #2c3e50;
}
ul {
list-style-type: none;
padding: 0;
}
li {
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #ddd;
}
li:hover {
background-color: #eaeaea;
}
li.selected {
background-color: #d0e6f7;
}
h3 {
margin: 0;
}
p {
margin: 0;
}
button {
margin-top: 10px;
padding: 5px 10px;
background-color: red;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background-color: darkred;
}
</style>
二、题目的自动刷新
我们上面实现了显示题目,但是每次添加完题目不能自动刷新左边的列表,这就很呆,需要用户手动刷新,肯定是不行的,于是我们需要每次更新数据之后刷新左边的列表。这里只涉及前端。
我们在组件挂载的时候,第一次调用fetchquestions,在每次更新之后,也调用,同时,我们将之前的Vue2文件改为Vue3的setup语法糖格式
home如下
<template>
<div class="container">
<div class="sidebar">
<QuestionList @selectQuestion="selectQuestion" :selectedQuestionId="selectedQuestion?.id" @refreshQuestions="refreshQuestions" ref="questionList" />
</div>
<div class="content">
<AddQuestion v-if="!selectedQuestion" @refreshQuestions="refreshQuestions" />
<EditQuestion v-if="selectedQuestion" :question="selectedQuestion" @refreshQuestions="refreshQuestions" @clearSelection="clearSelection" />
</div>
</div>
</template>
<script setup>
import { ref, getCurrentInstance, onMounted } from 'vue';
import QuestionList from '@/components/QuestionList.vue';
import AddQuestion from '@/components/AddQuestion.vue';
import EditQuestion from '@/components/EditQuestion.vue';
const selectedQuestion = ref(null);
const { proxy } = getCurrentInstance();
const selectQuestion = (question) => {
selectedQuestion.value = { ...question };
};
const clearSelection = () => {
selectedQuestion.value = null;
};
const refreshQuestions = () => {
clearSelection();
proxy.$refs.questionList.fetchQuestions();
};
onMounted(() => {
proxy.$refs.questionList.fetchQuestions();
});
</script>
<style>
.container {
display: flex;
height: 100vh;
}
.sidebar {
width: 500px;
background-color: #f4f4f4;
padding: 20px;
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
overflow-y: auto;
}
.content {
width: 750px;
padding: 20px;
overflow-y: auto;
}
</style>
在questionlist中,我们需要加入defineExpose({ fetchQuestions });
,暴露fetch方法,使得我们的父组件也可以调用它。
在add和edit中,每次请求后端操作后,需要加入
emit('refreshQuestions');
emit('clearSelection');
来调用方法,确保正确刷新questionlist。
三、修改问题
我们之前实现了增和删,现在我们加入改查
事实上,改的逻辑与增基本一致,我们放在一起。
我们加一个judge,如果judge是0,就还是我们原来的添加问题,如果是1就修改问题。
按照这个逻辑,我们修改蓝图中的get函数
def get(self):
judge = request.args.get("judge",None)
judge = int(judge)
if judge == 0:
question = request.args.get("question",None)
a = request.args.get("a",None)
b = request.args.get("b",None)
c = request.args.get("c",None)
d = request.args.get("d",None)
ap = request.args.get("ap",None)
bp = request.args.get("bp",None)
cp = request.args.get("cp",None)
dp = request.args.get("dp",None)
id = request.args.get("id",None)
try:
ob = sql_ex.sql_ex()
res = ob.add(question=question,a=a,b=b,c=c,d=d,ap=ap,bp=bp,cp=cp,dp=dp,id=id)
return jsonify( {"errcode":0,"msg":res} )
except:
return jsonify( {"errcode":1,"msg":res} )
elif judge == 1:
try:
question = request.args.get("question",None)
a = request.args.get("a",None)
b = request.args.get("b",None)
c = request.args.get("c",None)
d = request.args.get("d",None)
ap = request.args.get("ap",None)
bp = request.args.get("bp",None)
cp = request.args.get("cp",None)
dp = request.args.get("dp",None)
id = request.args.get("id",None)
ob = sql_ex.sql_ex()
res = ob.edit(question=question,a=a,b=b,c=c,d=d,ap=ap,bp=bp,cp=cp,dp=dp,id=id)
return jsonify( {"errcode":0,"msg":res} )
except:
return jsonify( {"errcode":0,"msg":"更改失败"} )
else:
return jsonify( {"errcode":1,"msg":"未知错误"} )
那我们前端的addques组件也需要响应增加
我们在前端的编辑组件中的edit方法里,这么写
async function editQuestion() {
try {
let result = await axios.get('http://127.0.0.1:5000/sqlex/', {
params: {
judge: 1,
id: localQuestion.value.id,
question: localQuestion.value.question_text,
a: localQuestion.value.choice_a,
b: localQuestion.value.choice_b,
c: localQuestion.value.choice_c,
d: localQuestion.value.choice_d,
ap: localQuestion.value.score_a,
bp: localQuestion.value.score_b,
cp: localQuestion.value.score_c,
dp: localQuestion.value.score_d,
}
});
window.alert(result.data.msg);
emit('refreshQuestions'); // 触发刷新问题列表的事件
} catch (error) {
alert(error);
}
}
同理,我们向后端发送get请求,并说明judge为1,这样就可以修改了。
四、删除题目
删除题目
我们先在后端蓝图中加入删除接口,来调用我们之前写好的sql删除工具。跟增加不一样,我们只要有id,就可以轻松删除我们的题目
def delete(self):
try:
id = request.args.get("id",None)
ob = sql_ex.sql_ex()
res = ob.delete(id=id)
return jsonify( {"errcode":0,"msg":"删除成功"} )
except:
return jsonify( {"errcode":1,"msg":"删除失败"} )
然后,我们在前端的完善deleteques函数
async function deleteQuestion() {
try{
let result = await axios.delete('http://127.0.0.1:5000/sqlex/',{params:{id:localQuestion.value.id}})
alert(result.data.msg)
emit('refreshQuestions');
emit('clearSelection');
}catch(error){alert(error)}
}
至此我们就完成了问题的增删改查。
五、使用pinia管理url
我们经常要调用后端接口,而我们后端的网址是会改变的,所以需要建立一个统一管理url的地方,不然你就需要挨个去改,十分吊诡。
我们在src下新建一个urlstore
import { defineStore } from 'pinia';
export const useUrlStore = defineStore('urlStore', {
state: () => ({
urls: {
sqlex: 'http://127.0.0.1:5000/sqlex/',
// 添加其他URL
}
}),
actions: {
updateUrl(key, newUrl) {
if (this.urls[key]) {
this.urls[key] = newUrl;
} else {
console.warn(`URL key ${key} not found`);
}
}
}
});
将urls放进去。
之后我们在main.ts中声明pinia
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia';
import router from './router'
//创建应用
const app = createApp(App)
//使用路由器
app.use(router)
//挂载app
const pinia = createPinia();
app.use(pinia);
app.mount('#app')
这样我们就可以在其他地方调用pinia来获取url啦
(上线的时候,全要换成公网ip,如果你不这样,倒时候一个一个改,十分容易出错。
六、token验证和自动登录
我们登录的时候会生成token用于表明用户的身份,如果用户退出了,理应保留一段时间的免登录,而且进行数据库查询的时候,也需要验证用户身份。
前端
我们在前端login登录的文件中,加入
onMounted( async()=>{
let token = localStorage.getItem("token")
if (token)
{
try{
let result=await axios.post(urlStore.urls.login,{
token:token,
})
if(result.data.errcode == 0){
window.alert(result.data.msg)
router.replace
setTimeout(()=>{router.push({path:"/home"})},1300)
}
}catch(error){}
}
} )
在登录组件挂载之后,判断是否存在token,如果存在,就自动跳转到login。这样我们又需要设置token的有效时间,不然会导致一旦登录,所有人在拿到这台电脑的时候都可以登录。
后端
打开后端的token_ex
import jwt
from datetime import datetime, timedelta, timezone
password = "1234"
class jwt_safe:
def __init__(self, data):
self.data = data
def encode(self):
try:
# 添加过期时间字段,设置为12小时后
# exp_time = datetime.now(timezone.utc) + timedelta(hours=12)
exp_time = datetime.now(timezone.utc) + timedelta(hours=6)
payload = {**self.data, 'exp': exp_time}
token = jwt.encode(payload, password, algorithm='HS256')
return token
except jwt.DecodeError:
# 令牌解码失败
return 'Invalid token'
except jwt.ExpiredSignatureError:
# 令牌过期
return 'Expired token'
def decode(self):
try:
payload = jwt.decode(self.data, password, algorithms=['HS256'])
return payload
except jwt.DecodeError:
# 令牌解码失败
return 'Invalid token'
except jwt.ExpiredSignatureError:
# 令牌过期
return 'Expired token'
if __name__ == '__main__':
data = {'msg': 123}
res = jwt_safe(data=data)
res = res.encode()
print(res)
res2 = jwt_safe(data=res)
res2 = res2.decode()
print(res2)
添加上时间,此处我设置了6小时有效,这样之后,token解码出来会同时又exp和account,只有exp时间内,才能被正确解码。
接下来,我们给login蓝图中加入对应逻辑
#验证token
def post(self):
data = request.get_json(silent=True)
if not data:
return jsonify( {"errcode":7,"msg":"未收到表单数据"} )
try:
token = data['token']
token = jwt_safe(token)
res = token.decode()
if res['account'] == '123456':
return jsonify( {"errcode":0,"msg":"自动登录成功"} )
except:
return jsonify( {"errcode":1} )
pass
这样就可以实现自动登录了~
七、数据库操作的token验证
懂点技术的人很容易得到你的后端接口,如果我们不在处理数据库操作的时候验证用户身份,就会被攻击打爆。
所以我们要在后端操作数据库蓝图中加入验证逻辑
尝试获取token(你也可以用try增加稳定性)只有token解码正确才可以继续。
同理在增删改查之前都这么做,完整代码如下
from flask import Blueprint, jsonify, request
from flask.views import MethodView
import sql_ex
from token_ex import jwt_safe
sqlex = Blueprint("sqlex", __name__)
class sqlex_(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":"无权限"} )
judge = request.args.get("judge",None)
judge = int(judge)
if judge == 0:
question = request.args.get("question",None)
a = request.args.get("a",None)
b = request.args.get("b",None)
c = request.args.get("c",None)
d = request.args.get("d",None)
ap = request.args.get("ap",None)
bp = request.args.get("bp",None)
cp = request.args.get("cp",None)
dp = request.args.get("dp",None)
id = request.args.get("id",None)
try:
ob = sql_ex.sql_ex()
res = ob.add(question=question,a=a,b=b,c=c,d=d,ap=ap,bp=bp,cp=cp,dp=dp,id=id)
return jsonify( {"errcode":0,"msg":res} )
except:
return jsonify( {"errcode":1,"msg":res} )
elif judge == 1:
try:
question = request.args.get("question",None)
a = request.args.get("a",None)
b = request.args.get("b",None)
c = request.args.get("c",None)
d = request.args.get("d",None)
ap = request.args.get("ap",None)
bp = request.args.get("bp",None)
cp = request.args.get("cp",None)
dp = request.args.get("dp",None)
id = request.args.get("id",None)
ob = sql_ex.sql_ex()
res = ob.edit(question=question,a=a,b=b,c=c,d=d,ap=ap,bp=bp,cp=cp,dp=dp,id=id)
return jsonify( {"errcode":0,"msg":res} )
except:
return jsonify( {"errcode":0,"msg":"更改失败"} )
else:
return jsonify( {"errcode":1,"msg":"未知错误"} )
def post(self):
temp = sql_ex.sql_ex()
temp = temp.search()
d2js={"data":temp}
print(d2js)
return jsonify(d2js)
def delete(self):
token =request.args.get("token")
token = jwt_safe(token)
res = token.decode()
print("\n\n",res,"\n\n")
if res['account'] != '123456':
return jsonify( {"errcode":1,"msg":"无权限"} )
try:
id = request.args.get("id",None)
ob = sql_ex.sql_ex()
res = ob.delete(id=id)
return jsonify( {"errcode":0,"msg":"删除成功"} )
except:
return jsonify( {"errcode":1,"msg":"删除失败"} )
sqlex.add_url_rule("/sqlex/", view_func=sqlex_.as_view("sqlex"))
总结
至此,我们彻底完成了管理员的后台
可以很好的进行增删改查的操作,方便不懂代码的网站管理员使用。
接下来,我们要制作用户视图(即问卷的展示部分),我们将使用赋分制,根据每阶段的得分来个性化给用户展示题目,经过一些处理,存进我们的数据库方便分析,敬请期待。
更多推荐
所有评论(0)