全栈开发一条龙——前端篇
第一篇:框架确定、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"))

总结

至此,我们彻底完成了管理员的后台

可以很好的进行增删改查的操作,方便不懂代码的网站管理员使用。
接下来,我们要制作用户视图(即问卷的展示部分),我们将使用赋分制,根据每阶段的得分来个性化给用户展示题目,经过一些处理,存进我们的数据库方便分析,敬请期待。

GitHub 加速计划 / vu / vue
80
16
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:4 个月前 )
73486cb5 * chore: fix link broken Signed-off-by: snoppy <michaleli@foxmail.com> * Update packages/template-compiler/README.md [skip ci] --------- Signed-off-by: snoppy <michaleli@foxmail.com> Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 6 个月前
e428d891 Updated Browser Compatibility reference. The previous currently returns HTTP 404. 6 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐