<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      考試系統前端項目復盤

      前段時間和朋友做了一個局域網考試系統,總共有3個端:考生端、監考端、管理端。

      框架與相關的庫

      先簡單說明一下我使用的框架和相關的庫:

      • 構建工具:Vite

      • 框架:Vue3

      • UI組件庫:element-plus

      • 網絡請求庫:axios

      • 路由跳轉:vue-router

      • 狀態管理:pinia

      • CSS擴展語言:sass

      • 其它與項目功能需求相關的庫這里就不一一列出了

      多端非根路徑部署

      考慮到每一個用戶理論上只會使用其中一個端,如果將三個端綁定在一個Vue項目上,則會導致“捆綁銷售”。因此,將三個端用三個Vue項目完成,然后讓后端開發人員使用nginx配置好映射。最后我需要再寫一個根路徑的入口頁面,用于跳轉到三個端。

      • /:根路徑,頁面的內容主要是三個按鈕,分別跳轉到三個端;
      • /admin:管理端;
      • /teacher:監考端;
      • /student:考生端。

      三個端的路徑經由nginx配置之后,指向三個Vue項目的index.html,然后再加載各自的main.js

      與以往將前端項目部署在根路徑的情況不同,將前端項目部署在非根路徑需要做相關配置。

      主要是需要修改vite.config.jsvue-router的配置文件。

      以管理端為例,由于其項目部署在/admin,因此需要配置項目的base

      vite.config.js

      export default defineConfig({
          ...
          base: '/admin/',
          ...
      })
      

      vue-router配置文件

      const router = createRouter({
          ...
          history: createWebHistory(import.meta.env.BASE_URL),
      	...
      })
      

      使用history模式,需要后端在nginx上做配置。而createWebHistory函數的參數需要傳入base,即上面配置的/admin/

      而余下的routes配置,就根據以往的編寫方式就可以。

      例如,管理端的登錄頁面,在配置了base: '/admin/'的情況下,在配置登錄頁面的路由的時候,只需要寫/login

      const router = createRouter({
        history: createWebHistory(import.meta.env.BASE_URL),
        routes: [
          {
            path: '/',
            redirect: {name: 'login'}
          },
          {
            path: '/login',
            name: 'login',
            component: ()=>import('../views/LoginView.vue')
          }
        ]
      })
      

      實際上,從整個考試系統的角度看來,它匹配到的路徑應該是:/admin/login

      這是因為/admin/會先被nginx的配置捕獲到,然后指向管理端這個Vue項目,返回管理端的index.htmlmain.js給用戶(該系統的管理員),然后路徑后續的/login會因為main.js中引入的路由配置文件,匹配上LoginView.vue,即登錄頁面。

      盒子的最大寬度

      頁面中的文字依據來源可以分為兩種:

      • 靜態文本:即本身固化在代碼中的文本;
      • 動態文本:由用戶輸入并顯示在頁面中的文本。

      靜態文本,例如側邊導航欄的按鈕的文本,文本的字數是固定的。因此,側邊導航欄的寬度可以寫成固定的。

      而動態文本,是由用戶輸入的,并且大多數時候沒有嚴格的字數限制。

      我一開始犯了一個錯,就是只使用flex:3flex:7簡單地將頁面分為左右布局,然后左邊是一個列表,每一項都是一行用戶輸入的數據,即不做換行處理。

      當用戶輸入了長文本之后,左邊的列表會被子元素撐大,從而導致頁面的左右布局比例被破壞。

      因此這里由用戶輸入的數據構成的列表,應該使用css設置一個max-width,限制其最大寬度。

      對象的深拷貝

      使用JSON簡單地實現了對象的深拷貝

      // 存儲對象的數組
      list: []
      
      // 添加新對象
      list.push(JSON.stringify(newItem))
      
      // 獲取對象
      function getItem(params){
          ...do some search
          return Json.parse(target)
      }
      

      pinia 實現試題管理模塊

      這里的試題是指添加試題時的階段,即需要提供讀與寫操作。

      image-20230909155800269

      • state:
      state: ()=>({
          // 題目列表,存儲題目對象,使用JSON簡單實現了對象的深拷貝
          qList: [],
          // 當前編輯的題目的指針
          currIdx: -1
      }),
      
      • getter:(返回常用數據)
      getters: {
          // 題目數量
          count(){
              return this.qList.length
          },
          // 當前編輯的題目是否存在“上一題”
          hasPrev(){
              return this.currIdx>0
          },
          hasNext(){
              return this.currIdx<this.count
          }
      },
      
      • actions:向外提供操作方法
      actions: {
          // 初始化
          init(){
              this.qList.length = 0
              this.currIdx = 0
          },
          // 寫操作
          saveQuestion(q){
              this.qList[this.currIdx] = JSON.stringify(q)
          },
          // 前一道題
          goPrevQuestion(){
              if(this.hasPrev){
                  return JSON.parse(this.qList[--this.currIdx] || "")
              }
          },
          // 后一道題
          goNextQuestion(){
              const q = this.qList[++this.currIdx]
              return q===undefined?undefined:JSON.parse(q)
          },
          // 上傳題目列表到后端
          async uploadQuestionList(){
              for await (let q of this.qList){
                  q = JSON.parse(q)
                  if(this.checkCompleteness(q)){
                      await uploadQuestion(q)
                  }
              }
          },
          checkCompleteness(q){
              // 用于檢查一道題目是否設置完整
          },
          isEmpty(q){
              // 用于檢查一道題目是否沒有填寫任何內容
          }
      }
      

      上述代碼中的checkCompletenessisEmpty函數的實現涉及到試題對象的設計,較為復雜,這里不給出代碼。

      上傳題目列表到后端的操作中,為了實現按順序上傳,需要使用for await(... of ...),而不能使用foreach await,后者無法保證上傳順序。

      試題編輯器——組件設計

      改考試系統的試題有五種題型:單選題、多選題、判斷題、簡答題、作圖題。

      每一種題目的數據結構不同,而又具有部分相同的視圖,故采用如下的設計:

      image-20230912121614686

      在試題編輯組件中,使用el-select組件選擇相應題型:

      // 當前編輯的題型
      const type = ref(1)
      // 題型列表
      const typeList  = reactive([
          {label: '單選題',value: 1,routeName: 'singleSelect'},
          {label: '多選題',value: 2,routeName: 'multiSelect'},
          {label: '判斷題',value: 3,routeName: 'booleanSelect'},
          {label: '簡答題',value: 4,routeName: 'shortAnswer'},
          {label: '作圖題',value: 5,routeName: 'drawPhoto'},
      ])
      

      五種題型的編輯器(用到了el-inputel-checkbox等組件)分為五個組件實現,并引入到QuestionEditor.vue中:

      <div class="editor-container">
          <!-- 編輯器 -->
          <single-select ref="singleSelectRef" v-if="type===1"/>
          <multi-select ref="multiSelectRef" v-if="type===2"/>
          <boolean-select ref="booleanSelectRef" v-if="type===3"/>
          <short-answer ref="shortAnswerRef" v-if="type===4"/>
          <draw-photo ref="drawPhotoRef" v-if="type===5"/>
      </div>
      

      由于試題的創建需要實現批量新增試題,需要實現“上一道題、下一道題”的切換,因此還需要實現試題的本地存儲,以及讀寫操作:

      • 讀操作在于存儲試題到store的時候,需要讀取試題對象;
      • 寫操作在于返回上一試題的時候,需要從store中讀取試題對象,并寫入到當前的編輯器中。

      額外地,這里實現了clear操作,即清空試題編輯器的功能。

      這是因為如果當前編輯中的試題是最后一題,那么點擊”下一道題“之后,就會開啟一個空白的試題。

      本質上就是做了一個clear操作。

      試題編輯器組件與上文的pinia實現的試題管理模塊是綁定的。

      本地存儲試題對象,使用JSON做簡單的深拷貝。

      試題的讀寫操作、清空操作的具體實現,在每一種試題對應的組件里,而QuestionEditor.vue組件只調用相應的接口:

      // 獲取題目內容(讀操作)
      const getQuestionHandler = ()=>{
          let q = {}
          q = editorRefList[type.value-1].value.getQuestion()
          q.type = type.value
          return q
      }
      // 設置題目內容(寫操作)
      const setQuestionHandler = (q)=>{
          nextTick(()=>{
              type.value = q.type
              nextTick(()=>{
                  editorRefList[type.value-1].value.setQuestion(q)
              })
          })
      }
      // 清空編輯器內容
      const clearQuestionHandler = ()=>{
          editorRefList[type.value-1].value.clear()
      }
      

      下面以單選題編輯組件為例,內部實現讀寫操作和清除操作:

      // 向外暴露獲取題目內容的方法
      const getQuestion = ()=>{
          return question
      }
      const setQuestion = (q)=>{
          ...
      }
      const clear = ()=>{
          ...
      }
      defineExpose({ getQuestion, setQuestion, clear })
      

      使用defineExpose向外暴露方法。

      vite打包配置

      vite.config.js中,通過如下配置,可以去除代碼中的console.log,避免將數據帶到生產環境,同時將js文件和assets文件打包到不同文件夾。

      export default defineConfig({
          ...
          build: {
              terserOptions: {
                  compress: {
                      // 生產環境時移除console.log調試代碼
                      drop_console:true,
                      drop_debugger: true
                  }
              },
              rollupOptions: {
                  output: {
                      //對靜態文件進行打包處理(文件分類)
                      chunkFileNames: 'assets/js/[name]-[hash].js',
                      entryFileNames: 'assets/js/[name]-[hash].js',
                      assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
                  }
              }
            }
          ...
      })
      

      文件下載功能

      項目中有需求是:用戶點擊按鈕之后下載文件。使用js實現:

      // 下載文件
      const downloadFile = () => {
          const tempDom = document.createElement('a')
          tempDom.href = "/file/demo.txt"
          tempDom.download = 'fileName.txt'
          tempDom.click()
      }
      

      這里創建了一個DOM對象,路徑href是服務器上的文件路徑,download屬性的字符串是用戶下載到的文件名。

      pdf預覽功能

      我寫了一個pdf-previewer.html文件,并放在根路徑下,然后每次不同端的項目中,需要訪問pdf文件的時候,就調用:

      window.open('/pdf-preview.html?url='+path)
      

      path是后端傳過來的文件路徑。

      pdf-previewer.html中,

      • 使用iframe標簽;
      • 封裝了getQueryVariable函數,用來獲取訪問地址攜帶的參數(即文件的地址);
      • 為了解決緩存問題(利用iframe打開pdf后,當再次利用iframe打開另一個pdf時會顯示第一份pdf,原因是瀏覽器對url的緩存處理),在url上添加時間戳。

      參考自:PDF預覽完整解決方案及各種兼容(VUE版) - 掘金 (juejin.cn)

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>PDF預覽窗口</title>
          <link rel="icon" href="/icon.png">
          <style>
              *{
                  margin: 0;
                  padding: 0;
                  box-sizing: border-box;
              }
              body{
                  width: 100%;
                  height: 100vh;
                  overflow: hidden;
              }
          </style>
      </head>
      <body>
          <iframe id="viewer" src="" style="width: 100%;height: 100vh;" frameborder="0"></iframe>
          <script>
              window.addEventListener ('load', () => {
                  function getQueryVariable(variable) {
                      let query = window.location.search.substring(1);
                      let vars = query.split("&");
                      for (let i = 0; i < vars.length; i++) {
                          let pair = vars[i].split("=");
                          if (pair[0] === variable) { return pair[1]; }
                      }
                      return (false);
                  }
                  
                  let path = getQueryVariable('url')
                  const fresh = new Date().getTime()
      
                  path += '?fresh=' + fresh
      
                  document.getElementById('viewer').setAttribute('src', path)
              });
          </script>
      </body>
      </html>
      

      常用Message封裝

      el-message組件對于反饋功能很常用,封裝成函數:

      import { ElMessage } from 'element-plus'
      
      const showError = (msg)=>{
          return ElMessage({
              type: 'error',
              message: msg
          })
      }
      
      const showSuccess = (msg)=>{
          return ElMessage({
              type: 'success',
              message: msg
          })
      }
      
      const showInfo = (msg)=>{
          return ElMessage({
              type: 'info',
              message: msg
          })
      }
      
      export { showError, showSuccess, showInfo }
      

      使用CSS常量

      使用CSS常量記錄常用的尺寸、顏色,可以改一處,而變全局。

      以下常量是我的項目中的一部分顏色,僅供參考,不具有普適性。

      :root {
        --main-color: #31364d;
        --header-height: 60px;
        --border-color: #DCDFE6;
        --border-color-light: #E4E7ED;
        --border-color-darker: #CDD0D6;
        --page-background: #F2F3F5;
      }
      

      響應式數組的數據更新

      不能直接將一個數組賦值給響應式數組,否則會失去響應式,而是應該:

      const list = reactive([])
      
      axios.get('...').then((newList)=>{
          list.length = 0
          list.push(...newList)
      })
      

      應該使用將長度設置為0,重新push新元素的方式進行數組更新。

      如果數組是對象數組,且后端返回的對象數組中,不是所有屬性都需要,則可以使用map方法進行選擇性地保留屬性:

      list.push(...newList.map(item=>({
          a: item.aa,
          b: item.bb,
          // 可以在后端返回的對象的基礎上,保留部分屬性,
          // 也可能添加僅前端需要的新屬性,即數據預處理
          newKey: 'xxx'
      })))
      

      最終打包

      最終打包項目給后端的時候,三個端,即三個Vue項目分別執行npm build進行打包,然后和

      • 根路徑入口頁面(index.html);
      • 靜態文件/static
      • 插件/plugins
      • 三個端共享的資源預覽頁面pdf-preview.html

      放在一個文件夾里。

      image-20230912154942088
      posted @ 2023-09-09 18:11  feixianxing  閱讀(232)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 黄瓜视频在线观看| 一区二区三区四区黄色片| 九九热精品视频免费在线| 日韩精品无码不卡无码| 自拍偷区亚洲综合第二区| 热久久99精品这里有精品| 香蕉EEWW99国产精选免费| 亚洲国产天堂久久综合网| 蜜臀av在线一区二区三区| 另类专区一区二区三区| 欧美性受xxxx白人性爽| 亚洲国产中文字幕在线视频综合 | 福利视频一区二区在线| 强奷乱码中文字幕| 国产二区三区不卡免费| 久久精品一本到99热免费| 日本japanese丰满白浆| 性xxxx搡xxxxx搡欧美| 四虎国产精品永久入口| 亚洲在av极品无码天堂| 强奷漂亮少妇高潮伦理| 横峰县| 国产高在线精品亚洲三区| 国产AV老师黑色丝袜美腿| 国产又色又爽又刺激在线观看| 日韩av日韩av在线| 亚洲日韩国产一区二区三区在线| 国产一区二区不卡在线| 张掖市| 狠狠色丁香婷婷综合尤物| 老鸭窝在钱视频| 无码h黄肉动漫在线观看| 伊人欧美在线| 少妇高潮水多太爽了动态图| 国产精品理论片| 日韩加勒比一本无码精品| 亚洲精品国产无套在线观| 国内精品卡一卡二卡三| 日韩国产中文字幕精品| 日韩精品一区二区蜜臀av| 无码一区二区三区中文字幕|