源碼地址vue
預覽地址(沒有作響應式,請在電腦上打開)git
使用了我自制的日曆組件(初學vue時作的,有些糙)calendar-inputgithub
參考設計圖數組
實現一個簡易版的問卷管理系統,有以下功能:模塊化
有一個頭部能夠顯示logo,不須要實現登陸等操做函數
問卷管理列表頁面默認爲首頁工具
有一個表格用於展現全部已建立的問卷fetch
列表中包括列有:問卷名稱,問卷狀態(未發佈,發佈中,已結束),和操做區域(編輯、刪除、查看數據)ui
問卷狀態爲未發佈時,能夠作的操做爲編輯、刪除、查看問卷this
問卷狀態爲發佈中和已結束時,能夠作的操做爲查看數據、查看問卷
表格最左側有批量選擇(多選)的checkbox,多選後,能夠進行批量刪除功能,checkbox樣式用默認便可,不須要按照設計圖的樣式
當一個問卷都沒有的時候,表格不展示,頁面顯示大大的新建問卷按鈕
點擊問卷管理列表中的新建按鈕後,進入到問卷新建頁面
點擊問卷列表中某個問卷行的編輯按鈕後,進入到問卷的編輯頁面
新建頁面和編輯頁面基本相同
問卷有一個標題字段,點擊後能夠進入編輯狀態
能夠針對問卷中的問題進行增刪改操做,每一個問卷最少一個問題,最多十個問題
問題類型包括:單選題、多選題、單行文本題
能夠對全部問題進行位置改變(上移、下移),複用,刪除的操做
最上面的問題沒有上移操做,最下面的問題沒有下移操做
點擊複用時,在被複用的問題緊接着的下方新增一個和被複用徹底同樣的問題(包括選項)
對於單選題和多選題,能夠對問題的選項進行增、刪、改、排序操做
文本題能夠設定是必填仍是非必填的問題
有一個問卷調查填寫截止時間,使用一個日曆組件來進行時間的選擇,日期選擇不能早於當前日期
保存問卷能夠進行問卷的保存
發佈問卷可使得問卷狀態變爲發佈中的狀態
當點擊發布時,若是截止日期早於當前日期,則須要提示修改截止日期
在問卷管理列表中點擊某個問卷的刪除按鈕後,彈出一個浮出層,讓用戶二次確認是否刪除該問卷,若是用戶點擊是,則刪除掉該問卷
在問卷管理列表中點擊查看問卷的按鈕後,在新窗口中打開該問卷的頁面,該頁面是可供用戶進行問卷填寫的頁面,在問卷未發佈狀態和已結束狀態時,問卷提交是無效的。
該頁面在移動端須要進行良好的兼容支持
在問卷管理列表中點擊查看數據按鈕後,進入到一個數據報告頁面,用圖表形式呈現各個單選題和多選題的選擇狀況
如設計稿中呈現,每個問題在右側用某種圖表來呈現答題狀況,自行選擇合適的圖表,設計稿中僅爲示意,圖表樣式不須要和設計稿一致。推薦單選題使用餅狀圖,多選題使用條形圖
文本題用一個百分比圖展示有效回答佔比便可
返回按鈕點擊後返回列表頁面
在項目中嘗試模塊化的方法及工具
在項目中嘗試CSS預處理工具
在項目中嘗試項目構建、打包工具
首先每一個列表項都使用了v-model進行雙向數據綁定傳遞是否被選中狀態
<template v-for="item in qsList"> <ul> <li><input type="checkbox" v-model="item.checked"></li>
而後給全選按鈕也用v-model綁定是否全選狀態
<label><input type="checkbox" id="all-check" v-model="selectAll">全選</label>
下一步在computed中定義三個計算屬性
selectAll: 是否全選
selectCount: 計算有多少項被選中
selectGroup: 存儲當前選中項,以便對它們進行操做
selectAll計算屬性:
selectAll: { get() { //this.qsList是一個數組,理解代碼時能夠看爲[{checked: false}, {checked: false}] return this.selectCount === this.qsList.length && this.selectCount !== 0; }, set(value) { this.qsList.forEach( item => { item.checked = value; } ); return value; } }
經過get方法獲取當前選中數,從而實現當列表項全被選中時,全選按鈕自動被選中
經過set方法實現當全選按鈕選中時,全部列表項也被選中
selectCount計算屬性
selectCount() { let i = 0; this.qsList.forEach( item => { if (item.checked) i++; } ); return i; },
計算當前有多少項被選中,selectAll經過此變量來計算當前是否全部列表項都被選中
selectGroup計算屬性
selectGroup() { let group = []; this.qsList.forEach( item => { if (item.checked) group.push(item); } ); return group; }
存儲被選中項,進行統一操做
這個問題我使用了v-model來解決,問卷中總共有三種類型的表單項,radio,checkbox,textarea 由於對於radio的v-model來講只能綁定一個基本類型的值, checkbox的v-model應該綁定一個數組,這樣選中項就會一個一個push到數組中,並且是雙向綁定的,textarea的v-model也應該是一個基本類型,我設置的是字符串
<p v-for="option in item.options" class="option"> <label> <input type="radio" :name="`${item.num}-${item.title}`" v-model="requiredItem[item.num]" v-if="item.type === 'radio'" :value="option"> <input type="checkbox" :name="`${item.num}-${item.title}`" v-model="requiredItem[item.num]" v-if="item.type === 'checkbox'" :value="option">{{option}} </label> </p> <textarea v-if="item.type === 'textarea'" v-model="requiredItem[item.num]"></textarea> //獲取必選項,用對象存儲起來,至關於 {1: '', 2: [], 3: ''} getRequiredItem() { this.qsItem.question.forEach( item => { if (item.isNeed) { if (item.isNeed) { if (item.type === 'checkbox') { this.requiredItem[item.num] = [] //多選框雙向綁定的值 } else { this.requiredItem[item.num] = '' //單選框 文本框雙向綁定的值 } } } } ) } //直接檢測雙向綁定的值的內容長度便可知道必填項是否有值 validate() { for (let i in this.requiredItem) { if (this.requiredItem[i].length === 0) return false } return true }
這裏還有一個問題,我如今在v-for中經過v-if來判斷表單項類型,這樣看起來有些冗餘,爲何不直接動態綁定type來渲染表單項呢,這樣就不用v-if了
<p v-for="option in item.options" class="option"> <input :type="item.type" :name="`${item.num}-${item.title}`" v-model="requiredItem[item.num]" :value="option"> </p>
這樣看起來簡潔多了,可是這樣寫會報錯,v-model不能綁定在type屬性爲動態值的表單項上,即type是bind的表單項不能用v-model,因此這裏只能退一步使用v-if來選擇渲染哪一種類型的表單項
當用戶點擊刪除某一項時,通常的作法時彈出一個彈出層詢問用戶是否刪除,用戶點擊肯定再進行刪除操做。這時只要給肯定按鈕綁定一個點擊事件進行刪除操做便可,可是當要屢次點擊肯定進行下一個步驟,或者頁面多個操做事件都是彈出這個彈出層,這時肯定按鈕就要去判斷綁定哪一個操做事件等等,很快就變得很是複雜起來
這裏可使用ES6的Generator函數,能夠很方便的解決這個問題
<div class="shadow" v-if="showDialog"> <div class="del-dialog"> <header> <span>提示</span> <span class="close-btn" @click="showDialog = false">X</span> </header> <p>{{info}}</p> <div class="btn-box"> <button class="yes" @click="iterator.next();">肯定</button> <button @click="showDialog = false">取消</button> </div> </div> </div>
彈出層內容
data() { return { qsList: [], showDialog: false, //是否顯示彈出層 iterator: {}, //當前迭代器 info: '' //彈出層提示內容 } }
data中的數據
*delItem(num) { yield this.showDialogMsg('確認要刪除此問卷') yield (() => { let index = 0; for (let length = this.qsList.length; index < length; index++) { if (this.qsList[index].num === num) break; } this.qsList.splice(index, 1); this.showDialog = false; })(); }, *delItems() { yield this.showDialogMsg('確認要刪除選中的問卷?'); yield (() => { this.showDialog = false; if (this.selectAll) { this.qsList = []; return; } if (this.selectGroup == []) return; this.selectGroup.forEach( item => { if (this.qsList.indexOf(item) > -1) this.qsList.splice(this.qsList.indexOf(item), 1); } ) })(); }, *edit(item) { yield (() => { if (item.state === 'noissue') { this.showDialogMsg('確認要編輯?'); } else { this.showDialogMsg('只有未發佈的問卷才能編輯'); } })(); yield (() => { if (item.state !== 'noissue') { this.showDialog = false; } else { this.showDialog = false; this.$router.push({name: 'qsEdit', params: { num: item.num }}) } })(); }, *watchData(item) { yield (() => { if (item.state === 'noissue') { this.showDialogMsg('未發佈的問卷無數據可查看'); } else { this.$router.push({ name: 'qsData', params: { num: item.num }}) } })(); yield this.showDialog = false; }
能夠看到 頁面中多個操做都綁定在一個彈出層上,實現最大程度的複用,並且不會衝突,只要把當前要執行的操做的迭代器賦給肯定按鈕,肯定按鈕執行next方法便可
有時咱們須要v-for的每次遍歷中就執行一個函數,咱們能夠這樣
<li v-for="item in data">{{doSomething()}}</li>
可是這種作法若是執行比較複雜的方法很容易出現一些錯誤好比無限循環等錯誤,並且也不推薦
根據須要能夠考慮在js中再次遍歷這個數據而後在遍歷中對每一項進行操做
在編輯問卷功能中,題目號應該要根據題目的上移下移複用刪除新建等操做進行變化,我使用了watch來監測變化而後更改題號
watch: { '$route': 'fetchData', qsItem: { handler(newVal) { newVal.question.forEach( (item, index) => { item.num = `Q${index + 1}` } ) }, deep: true } }
我在進行上移,下移,刪除,新建問題等操做時都沒有問題,可是在複用操做時產生了無限循環的問題
<div class="questions" v-for="(qs, index) in qsItem.question"> <span @click="copy(index, qs)">複用</span> </div>
複用按鈕,和複用方法
copy(index, qs) { if (this.questionLength === 10) return alert('問卷已滿!') this.qsItem.question.splice(index, 0, qs) }
這樣寫看起來沒什麼問題,哪一個item下的複用按鈕被點擊,就將這個item添加到本身下一項。
可是qs添加到watch監測的變量中後,會觸發watch的方法,更改題目號,即qs的題目號被更改,同時qs又是那個被點擊的item,它們之間存在引用,這就會形成qs題目號的更改會使點擊的item的題目號跟着一塊兒變化,這樣item一變化,watch又被觸發,同時item的題目號由於跟着一塊兒變化,致使題目號不是它正確的題目號,watch觸發後,item的題目號又會變化爲原來的,由於存在引用qs的又會跟着變,而後再次觸發watch....一直循環下去
解決方法是用Object.assign()進行一次深拷貝,這樣qs和item之間就不存在引用了
copy(index, qs) { if (this.questionLength === 10) return alert('問卷已滿!') qs = Object.assign({}, qs) this.qsItem.question.splice(index, 0, qs) }
這種作法不推薦,由於這種狀況下使用watch原本就是不該該的,很是容易形成想不到的問題
推薦的作法是將watch中的方法封裝成一個函數,每次操做時就調用這個函數,固然仍是須要Object.assign()來解除複用元素之間的綁定
這裏我爲了練習仍是使用了watch這個不推薦的方法
總結完成,交做業了