一直想把一大篇的總結寫完、寫好,感覺自己拖延太嚴重還總想寫完美,然後好多筆記都死在編輯器裡了,以後還按照一個小節一個小節的更新吧,小步快跑?,先發出來,以後再迭代吧。
最近我們參與開發了一個(年前了)BI專案,前端使用vue全家桶,專案功能基本開發完成,剩下的修修補補,開發過程還算順暢,期間遇到好多問題,也記錄了一下,發出來一起交流,主要是思路,怎麼利用vue給的API實現功能,避免大家在同樣的坑裡待太長時間,如果有更好實現思路可以一起交流討論??。
前後端分離形式開發,vue+vueRouter+vueX+iviewUI+elementUI
,大部分功能我們都用的iviewUI,有部分元件我們用了elementUI,比如表格、日曆外掛,我們沒接mock工具,介面用文件的形式交流,團隊氛圍比較和諧,三個PHP三個前端,效率還可以,兩個前端夥伴比較厲害,第一次使用vue,就承擔了90%的開發工作任務,我沒到上線就跑回家休陪產假了,特別感謝同事們的支援,我才能回家看娃。
前端其實不太複雜,但是隻要用vue開發基本上都會遇到的幾個問題,比如選單元件多級巢狀、重新整理後選中當前項、
涉及幾個點,表格表頭表體合併、檔案上傳、富文字編輯器、許可權樹等等。
專案介紹
系統的主要功能就是面向各個部門檢視報表資料,後端同學們很厲害,能彙總到一個集團的所有資料,各種炫酷大資料技術;
選單功能:
- 資料看板: 篩選、展示日期和表格分頁
- 業務報表: 報表型別,日期篩選、表格分頁
- 資料檢索: 篩選項聯動、表格分頁
- 損耗地圖: 篩選項、關係圖外掛
- 展開分析: 篩選項、分類、卡片、表格
- 系統資訊: 版本釋出、步驟條、富文字編輯
- 資料來源上傳: 手動上傳、表格展示
- 許可權管理: 使用者管理、角色管理(許可權選單配置)
專案預覽圖:
對勾為已更新。
- 1. 使用v-if解決非同步傳參
- 2. 使用$refs呼叫子元件方法
- 3. 元件遞迴實現多級選單
- 4. 使用watch監聽路由引數重新獲取資料
- 5. 頁面重新整理後Menu根據地址選中當前選單項
- 6. 使用Axios統一狀態碼判斷、統一增加token欄位
- 7. 點選左側選單選中項點選重新整理頁面
- 8. 使用Axios.CancelToken切換路由取消請求
- 9. 使用element的table元件實現 表頭表體合併
- 10. iview的Menu元件+vuex實現麵包屑導航
- 11. iview上傳元件手動上傳與富文字編輯器接入
- 12. 使用cheerio獲取表格資料
- 13. keep-live元件快取
- 14. 讓資料保持單向流動(不要在子元件中操作父元件的資料)
1. 使用v-if解決非同步傳參元件重繪
大部分的互動的流程都是 “ajax請求資料=>傳入元件渲染”,很多屬性需要非同步傳入子元件然後進行相關的計算,如果繫結很多computed或者watch,效能開銷會很大,而且有些場景並不需要使用computed和watch,我們只需要在最初建立的時候獲取一次就夠了。
如下gif例子,點選上方TAB後重新重新整理折線元件:
<!--模板-->
<mapBox v-if="mapData" :data="mapData"></mapBox>
複製程式碼
<!--點選搜尋後執行-->
let This = this
// setp1 重點
this.mapData = false
this.$http
.post('/api/show/mapcondition',{key:key,type:type})
.then(function(response){
// setp2 重點
this.mapData = response.data
})
複製程式碼
有時候會出現DOM元素與資料不同步,可以使用使用其他方式讓DOM強刷
- setTimeou
- $forceUpdate()
- $nextTick()
- $set()
複製程式碼
2. 使用$refs呼叫子元件方法
有時候會涉及到父元件呼叫子元件方法的情況,例如,iview的Tree元件暴露出來的getCheckedAndIndeterminateNodes
方法,詳見官網文件link。
<!--模板-->
<Tree v-if="menu" :data="menu" show-checkbox multiple ref="Tree"></Tree>
複製程式碼
let rules = this.$refs.Tree.getCheckedAndIndeterminateNodes();
複製程式碼
3. 元件遞迴實現多級選單
遞迴元件用的很多,我們的左側選單還有無限拆分的表格合併,都用到了遞迴元件,詳見官網連結link。
效果圖:
大致思路就是先建立一個子元件,然後再建立一個父元件,迴圈引用,拿左側選單說明,程式碼如下,資料結構也在父元件中。
<!--index.vue 父元件 資料介面在default中-->
<template>
<Menu width="auto"
theme="dark"
:active-name="activeName"
:open-names="openNames"
@on-select="handleSelect"
:accordion="true"
>
<template v-for="(item,index) in items">
<side-menu-item
v-if="item.children&&item.children.length!==0"
:parent-item="item"
:name="index+''"
:index="index"
>
</side-menu-item>
<menu-item v-else
:name="index+''"
:to="item.path"
>
<Icon :type="item.icon" :size="15"/>
<span>{{ item.title }}</span>
</menu-item>
</template>
</Menu>
</template>
<script>
import sideMenuItem from '@/components/Menu/side-menu-item.vue'
export default {
name: 'sideMenu',
props: {
activeName: {
type: String,
default: 'auth'
},
openNames: {
type: Array,
default: () => [
'other',
'role',
'auth'
]
},
items: {
type: Array,
default: () => [
{
name : 'system',
title : '資料看板',
icon : 'ios-analytics',
children: [
{ name : 'user', title : '使用者管理', icon : 'outlet',
children : [
{ name : 'auth', title : '許可權管理1', icon : 'outlet' },
{ name : 'auth', title : '許可權管理', icon : 'outlet',
children:[
{ name : '334', title : '子選單', icon : 'outlet' },
{ name : '453', title : '子選單', icon : 'outlet' }
]
}
]
}
]
},
{
name : 'other',
title: '其他管理',
icon : 'outlet',
}
]
}
},
components: {
sideMenuItem
},
methods: {
handleSelect(name) {
this.$emit('on-select', name)
}
}
}
</script>
複製程式碼
<!--side-menu-item.vue 子元件-->
<template>
<Submenu :name="index+''">
<template slot="title" >
<Icon :type="parentItem.icon" :size="10"/>
<span>{{ parentItem.title }}</span>
</template>
<template v-for="(item,i) in parentItem.children">
<side-menu-item
v-if="item.children&&item.children.length!==0"
:parent-item="item"
:to="item.path"
:name="index+'-'+i"
:index="index+'-'+i"
>
</side-menu-item>
<menu-item v-else
:name="index+'-'+i" :to="item.path">
<Icon :type="item.icon" :size="15" />
<span>{{ item.title }}</span>
</menu-item>
</template>
</Submenu>
</template>
<script>
export default {
name: 'sideMenuItem',
props: {
parentItem: {
type: Object,
default: () => {}
},
index:{}
},
created:function(){
}
}
</script>
複製程式碼
4. 使用watch監聽路由引數重新獲取資料
很多選單項都只是入參不一樣,是不會重新走業務邏輯的,我們就用watch監聽$router,如果改變就重新請求新的資料。
export default {
watch: {
'$route':'isChange'
},
methods:{
getData(){
// Do something
},
isChange(){
this.getData()
},
}
}
複製程式碼
5. 重新整理:根據地址選中當前選單項
頁面重新整理後左側選單的預設選中項就和頁面對應不上了,我們用$router的beforeEnter方法做判斷,根據地址獲得路由的key(每一個路由都有一個key的引數),儲存到localStorage中,然後選單元件再從localStorage中取出key,再遍歷匹配到當前選專案,比較冗餘的是我們要在beforeEnter中獲取一遍選單資料,然後到選單元件又獲取一次資料,請求兩次介面。
step1 router.js中設定beforeEnter方法,獲得位址列中的key 儲存到localStorage
step2 選單元件取出localStorage中key,遞迴匹配
複製程式碼
6. Axios統一狀態碼判斷、統一增加token欄位
Axios的interceptors方法有request和response兩個方法對請求的入參和返回結果做統一的處理。
<!--request 除登入請求外,其他均增加token欄位 -->
axios.interceptors.request.use(function (config) {
let token = localStorage.getItem('token')
if(token== null && router.currentRoute.path == '/login'){// 本地無token,未登入 跳轉至登入頁面
router.push('/login')
}else{
if(config.data==undefined){
config.data = {
"token":token
}
}else{
Object.assign(config.data,{"token":token})
}
}
return config
}, function (error) {
iView.Message.error('請求失敗')
return Promise.reject(error)
})
<!--response 返回狀態統一處理 -->
axios.interceptors.response.use(function (response) {
if(response.hasOwnProperty("data") && typeof response.data == "object"){
if(response.data.code === 998){// 登入超時 跳轉至登入頁面
iView.Message.error(response.data.msg)
router.push('/login')
return Promise.reject(response)
}else if (response.data.code === 1000) {// 成功
return Promise.resolve(response)
} else if (response.data.code === 1060){ //資料定製中
return Promise.resolve(response)
}else {// 失敗
iView.Message.error(response.data.msg)
return Promise.reject(response)
}
} else {
return Promise.resolve(response)
}
}, function (error) {
iView.Message.error('請求失敗')
// 請求錯誤時做些事
return Promise.reject(error)
})
複製程式碼
7. 點選左側選單選中項點選重新整理頁面
測試同學提出bug,左側選單選中後,再次點選選中項沒有重新整理,使用者體驗不好,產品同學一致通過,我們就用野路子來解決了。 給選單元件設定on-select事件,點選後儲存當前選中項的path,每次執行當前點選的path和儲存的path做對比,如果一致,跳轉到空白頁,空白頁再返回到當前頁,實現假重新整理,注:不知道是router.push有節流控制還是怎麼回事,不加setTimeout不管用。
<!--選單的handleSelect事件-->
handleSelect(name) {
let This = this
if((this.selectIndex == 'reset') || (name == this.selectIndex)){
// 點選再次重新整理
setTimeout(function function_name(argument) {
This.$router.push({
path: '/Main/about',
query: {
t: Date.now()
}
})
},1)
}
this.selectIndex = name
this.$emit('on-select', name)
},
複製程式碼
<!--空白頁-->
created(){
let This = this
setTimeout(function function_name(argument) {
This.$router.go(-1);
},1)
}
複製程式碼
8. 使用Axios.CancelToken切換路由取消請求
有一部分情況是切換路由時,只改變引數,在“4. 使用watch監聽路由引數重新獲取資料”中提到過,還有一部分功能的介面資料返回的特別慢,會出現切換選單後,資料才載入出來,需要增加切換選單後取消原來的請求,程式碼註釋中 setp1、2、3為順序
export default {
data(){
return {
// setp1 建立data公共的source變數
source:''
}
},
created:function(){
// 獲取搜尋資料
this.getData()
},
watch:{
'$route':'watchGetSearchData',
},
methods:{
getData(){
// setp2 請求時建立source例項
let CancelToken = this.$http.CancelToken
this.source = CancelToken.source();
},
watchGetSearchData(){
// setp3 切換路由時取消source例項
this.source.cancel('0000')
this.getData()
this.$http
.post('/api/show/map',data,{cancelToken:this.source.token})
.then(function(response){
})
}
}
}
複製程式碼
9. element的table元件實現 表頭表體合併
我們專案用到的的元件表格有兩種,一種用iview的table,帶操作按鈕的表格,支援表頭跨行跨列,另一種element的table元件,純資料展示,支援表頭和標題的跨行跨列。
element的table元件支援表頭標題合併,我們定義資料結構包含三部分,表頭、表體、表體合併項。 表頭直接使用遞迴元件巢狀就可以了,表體資料直接扔給table元件,合併通過cellMerge方法遍歷合併項資料遍歷合併,程式碼如下。
資料結構
data:{
historyColumns:[ // 表頭資料
{
"title": " ",
"key": "column"
},
{
"title": "指標",
"key": "target"
},
{
"title": "11/22",
"key": "11/22"
},
{
"title": "日環比",
"key": "日環比"
},
{
"title": "當週值",
"key": "當週值"
},
{
"title": "上週同期",
"key": "上週同期"
},
{
"title": "周環比",
"key": "周環比"
},
{
"title": "近7日累計",
"key": "近7日累計"
},
{
"title": "當月累計",
"key": "當月累計"
}
],
histories:[ // 表體資料
{
"target": "在售量",
"11/22": 912,
"日環比": "-",
"當週值": 912,
"上週同期": 0,
"周環比": "100%",
"近7日累計": 912,
"當月累計": 912,
"column": "基礎指標"
},
{
"target": "-在售外庫車量",
"11/22": 29,
"日環比": "-",
"當週值": 29,
"上週同期": 0,
"周環比": "100%",
"近7日累計": 29,
"當月累計": 29,
"column": "基礎指標"
}
],
merge:[ // 表體合併項
{
"rowNum": 0,
"colNum": 0,
"ropSpan": 1,
"copSpan": 4
},
{
"rowNum": 4,
"colNum": 0,
"ropSpan": 1,
"copSpan": 27
}
]
}
複製程式碼
表體合併說明: 表格有cellMerge方法,每一td在渲染時都會執行這個方法,在cellMerge裡遍歷merge資料,根據cellMerge的入參行、列定位到td,如果是要合併的表格,則return出要合併的行數和列數,如果在合併的範圍內,則要return [0,0],隱藏當前td。
比如要把A、B、C、D,merge的資料rowNum為A的行、colNum為A的列、ropSpan為2、copSpan為2,在cellMerge方法中,如果座標為A的單元格,return ropSpan和copSpan,如果座標為B、C、D則要return [0,0]隱藏,否則會出現表格錯亂。
merge方法程式碼:// 表格合併主方法 row:行陣列 column:列資料 rowIndex、columnIndex行列索引
cellMerge({ row, column, rowIndex, columnIndex }) {
let This = this;
if(This.configJson){
for(let i = 0; i < This.configJson.length; i++){
let rowNum = This.configJson[i].rowNum // 行
let colNum = This.configJson[i].colNum // 列
let ropSpan = This.configJson[i].ropSpan // 跨列數
let copSpan = This.configJson[i].copSpan // 跨行數
if(rowIndex == rowNum && columnIndex == colNum ){// 當前表格index 合併項
return [copSpan,ropSpan]
// 隱藏範圍內容的單元格
// 行範圍 rowNum <= rowIndex && rowIndex < (rowNum+copSpan)
// 列範圍 colNum <= columnIndex && columnIndex < (colNum+ropSpan)
}else if( rowNum <= rowIndex && rowIndex < (rowNum+copSpan) && colNum <= columnIndex && columnIndex < (colNum+ropSpan) ){
return [0,0]
}
}
}
}
複製程式碼
**表頭合併說明:**element和iview的表頭合併資料格式可以一樣,都是遞迴形式,區別是iview的table元件直接把資料扔給元件就可以了,而element需要自己封裝一下表頭。
// 子元件
<template>
<el-table-column :prop="thList.key" :label="thList.title" align="center">
<template v-for="(item,i) in thList.children" >
<tableItem v-if="item.children&&item.children.length!==0"
:thList="item" /></tableItem>
<el-table-column align="center" v-else
:prop="item.key"
:label="item.title"
:formatter="toThousands"
>
</el-table-column>
</template>
</el-table-column>
</template>
<script>
export default {
name: 'tableItem',
props: {
thList: {
type: Object,
default: () => {}
},
},
}
</script>
複製程式碼
封裝後的table元件:
<template>
<div>
<el-table :data="Tbody" :stripe="stripe" :border="true" :span-method="cellMerge" align="center" :header-cell-style="tableHeaderColor" height="600" >
<template v-for="(item,i) in Thead">
<template v-if="item.children&&item.children.length!==0" >
<tableItem :thList="item" />
</template>
<template v-else >
<el-table-column align="center"
:prop="item.key"
:label="item.title"
:formatter="toThousands"
>
</el-table-column>
</template>
</template>
</el-table>
</div>
</template>
<script>
import tableItem from '@/components/table/tableHeader/table-Item.vue'
export default {
name: 'table-header',
props: {
Thead: {
type: Array,
default: () => {}
},
Tbody:{
type: Array,
default: () => {}
},
stripe:{
type:Boolean,
default:false
},
cellMerge:Function,
default:()=>{}
},
created:function(){
},
components:{
tableItem
},
methods:{
tableHeaderColor({ row, column, rowIndex, columnIndex }) {
if (rowIndex === 0) {
return 'background-color: #f8f8f9;'
}
}
}
}
</script>
複製程式碼
其他頁面複用table
<!--引入-->
import TableList from '@/components/table/tableHeader/index.vue'
<!--呼叫-->
<TableList :Thead="historyColumns" :Tbody="historyData" :cellMerge="cellMerge" />
複製程式碼
10. iview的Menu元件+vuex實現麵包屑導航
iview的Menu元件有on-select方法,可以獲得當選選中項的name,我們的name按照資料索引來遍歷的,比如三級選單,選中後會返回2-0-1
這樣的字串,表示樹選單第3個選單下的第1個子選單下的第2個選單項,通過這個字串再篩選出陣列['業務報表','B2C報表','成交明細']
對應選單的title,然後發給vuex的Store.state,然後麵包屑元件通過計算資料屬性監聽Store.state拿屬性展示就可以了。
<!-- 根據字串篩出title陣列 發給$store -->
toBreadcrumb(arrIndex){
let This = this;
let mapIndex = arrIndex.split('-');
// 獲取對應name
let box={};
let mapText = mapIndex.map(function(item,index){
if(index == 0){
box = This.MenuData[eval(item)];
}else{
box = box.children[eval(item)];
}
return box.title;
});
this.$store.commit('toBreadcrumb',mapText)
}
複製程式碼
vueX程式碼
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
Breadcrumb:[], // 麵包屑導航
userName: '',
readyData:""
},
mutations: {
toBreadcrumb(state,arr){
state.Breadcrumb = arr;
}
},
getters: {
getBreadcrumb: state => {
return state.Breadcrumb
}
}
})
複製程式碼
麵包屑元件
<template>
<Header style="background: #fff;">
<Row>
<Col span="12">
<!-- {{doneTodosCount}} -->
<Breadcrumb>
<BreadcrumbItem v-for="item in doneTodosCount">{{item}}</BreadcrumbItem>
</Breadcrumb>
</Col>
<Col span="12">
<Login />
</Col>
</Row>
</Header>
</template>
<script>
import Login from '@/components/Login'
export default {
data(){
return {
}
},
created:function(){
this.$store.commit('toBreadcrumb',['首頁'])
},
computed: {
doneTodosCount () {
return this.$store.state.Breadcrumb
}
},
components:{
Login
}
}
</script>
複製程式碼
11. iview上傳元件手動上傳,接入富文字編輯器
iview提供的元件特別豐富,我們在做圖片上傳的時候,需要手動上傳,需要呼叫子元件的file物件通過自己的post方法提交到服務端,actionDate為檔案資料,然後再通過on-success回撥反饋上傳成功或失敗。 手動上傳:
<Upload
ref="upload"
:data= "actionDate"
:on-success="handleSuccess"
:format="['png','jpg']"
action="/api/upload/ccupload">
<Button icon="ios-cloud-upload-outline">點選上傳檔案</Button>
</Upload>
<div v-if="file !== null">
上傳檔案: {{ file.name }}
<Button type="text" @click="upload" :loading="loadingStatus">{{ loadingStatus ? 'Uploading' : '上傳' }}</Button>
</div>
複製程式碼
// upload 方法
let uploadFile = this.$refs.upload.file
this.$refs.upload.data = this.actionDate;
this.$refs.upload.post(uploadFile);
this.loadingStatus = true;
// handleSuccess 方法
this.loadingStatus = false;
if(res.code == 1000){
this.$Message.success('上傳成功')
}else{
this.$Message.error('上傳失敗')
}
複製程式碼
我們在聯調的過程中後端說接收不到檔案,我們只能用node來驗證一下是不是元件有問題,於是用express寫了一下檔案上傳。
var express = require('express');
var router = express.Router();
let fs = require('fs')
var formidable = require('formidable');//表單控制元件
var path = require('path');
var app = express();
app.use(express.static('/public/'));
router.post('/test',(req,res)=>{
var imgPath = path.dirname(__dirname) + '/public';
var form = new formidable.IncomingForm();
form.encoding = 'utf-8'; //設定編輯
form.uploadDir = imgPath; //設定上傳目錄
form.keepExtensions = true; //保留字尾
form.maxFieldsSize = 2 * 1024 * 1024; //檔案大小
form.type = true;
form.parse(req, function(err, fields, files){
let src = files.img.path.split('/');
let urlString = src[src.length-1]
if (err) {
console.log(err);
req.flash('error','圖片上傳失敗');
return;
}
res.json({
code: '200',
type:'single',
url:'http://10.70.74.167:3000/'+urlString
})
});
});
module.exports = router;
複製程式碼
我們在測試的時候增加了一個圖片test的轉發配置,然後把元件的action地址替換一下為/test/
就可以了,親測無問題[陰險臉]。
vue.config.js
module.exports = {
baseUrl: baseUrl,
devServer: {
proxy: {
'/api': { // 開發伺服器
target: ' http://*******',
changeOrigin: true,
},
'/test': { // 圖片上傳測試
target: ' http://10.70.74.167:3000',
changeOrigin: true,
}
}
},
productionSourceMap: false,
}
複製程式碼
富文字編輯器的圖片上傳有兩種模式,一種是把圖片轉成base64,通過一個介面把html內容提交給服務端,另一種模式是兩個介面,分別把圖片上傳到伺服器,然後返回url字串到編輯器中,再把編輯器中的html儲存到伺服器上,我們用的編輯器是vue-quill-editor
,使用第二種模式,藉助element的el-upload元件自動上傳圖片,然後返回地址插入到編輯器。
import {format} from '@/lib/js/utils.js'
import {quillEditor} from 'vue-quill-editor'
const toolbarOptions = [
['bold', 'italic', 'underline', 'strike'], // toggled buttons
[{'header': 1}, {'header': 2}], // custom button values
[{'list': 'ordered'}, {'list': 'bullet'}],
[{'indent': '-1'}, {'indent': '+1'}], // outdent/indent
[{'direction': 'rtl'}], // text direction
[{'size': ['small', false, 'large', 'huge']}], // custom dropdown
[{'header': [1, 2, 3, 4, 5, 6, false]}],
[{'color': []}, {'background': []}], // dropdown with defaults from theme
[{'font': []}],
[{'align': []}],
['link', 'image'],
['clean']
]
export default {
data () {
return {
options2:{},
quillUpdateImg: false, // 根據圖片上傳狀態來確定是否顯示loading動畫,剛開始是false,不顯示
content:'', // 富文字內容
title:'新建',
editorOption:{
placeholder: '',
theme: 'snow', // or 'bubble'
modules:{
toolbar: {
container: toolbarOptions,
handlers: {
'image': function (value) {
if (value) {
// 觸發input框選擇圖片檔案
document.querySelector('.avatar-uploader input').click()
} else {
this.quill.format('image', false);
}
}
}
}
}
},
serverUrl: '/api/add/upload?key='+this.$route.params.key, // 這裡寫你要上傳的圖片伺服器地址
header: {
// token: sessionStorage.token
},
current: 0,
formValidate: {
device_name: '集團BI',
versions: '',
publish_time: '',
desc: '',
},
ruleValidate: {
device_name: [
{ required: true, message: '請選擇系統名稱', trigger: 'change' }
],
versions: [
{ required: true, message: '請輸入版本資訊', trigger: 'blur' }
],
publish_time: [
{ required: true, type: 'date', message: '請選擇發版時間', trigger: 'change' }
],
desc: [
{ required: true, message: '請輸入對於該版本的總體描述', trigger: 'blur' },
{ type: 'string', min: 20, message: '版本的總體描述不少於20個字', trigger: 'blur' }
]
},
isFirst: true,
isSecond: false,
isThird: false,
versionid:''
}
},
created:function(){
this.limit();
this.initialization();
},
methods: {
limit(){
this.options2 = {
disabledDate (date) {
return (date && date.valueOf() > new Date().getTime()) || (date && date.valueOf() < new Date("2017-12-31"))
}
}
},
//初始判定是新增/修改
initialization(){
let id = this.$route.params.id;
if(id !=0){
this.title = "編輯";
let obj = {};
obj.version_id = this.$route.params.id;
obj.key = this.$route.params.key;
this.$http
.post('/api/show/version',obj).then(response => (
this.formValidate.device_name = response.data.data.device_name,
this.formValidate.versions = response.data.data.versions,
this.formValidate.publish_time = response.data.data.publish_time,
this.formValidate.desc = response.data.data.desc,
this.content = response.data.data.pc_html
))
}else{
this.title = "新建";
}
},
//第一步基本資訊(釋出)
firstSubmit(name){
this.$refs[name].validate((valid) => {
if (valid) {
this.$Message.success('資訊新增成功');
this.current += 1;
this.isFirst = !this.isFirst;
this.isSecond = !this.isSecond;
}else{
this.$Message.error('請完善必填資訊');
}
})
},
//第二步的表單資料提交(釋出)
save(){
let id = this.$route.params.id;
let addObj = this.formValidate;
addObj.publish_time = format(this.formValidate.publish_time);
addObj.pc_html = this.content;
addObj.key = this.$route.params.key;
if(this.$route.params.id != 0){
addObj.version_id = id;
}
this.$http
.post('/api/add/version',addObj).then(response => (
this.secondSubmit(response.data.version_id)
))
},
//第二步提交成功後轉至第三步(釋出)
secondSubmit(id){
this.current += 1;
this.isSecond = false;
this.isThird = !this.isThird;
this.versionid = id;
},
//第三步跳轉至[預覽]
preview(){
this.$router.push({ path:"/Main/VersionManagementInfo/system_versions/"+this.versionid});
},
//第三步釋出
release(){
let status = this.$route.params.status;
if(status != 2){
let obj = {};
if(this.$route.params.id == 0){
obj.version_id = this.versionid;
}else{
obj.version_id = this.$route.params.id;
}
obj.key = this.$route.params.key;
this.$http
.post('/api/edit/publish/version',obj).then(response => (
this.releaseLink()
))
}else{
this.releaseLink()
}
},
//第三步釋出跳轉
releaseLink(){
this.$router.push({ path:"/Main/VersionManagement/system_versions"});
},
//上一步操作
returns () {
if (this.current != 0) {
this.current -= 1;
this.isFirst = true;
this.isSecond = false;
}
},
//富文字內容改變事件
onEditorChange({editor, html, text}) {
this.content = html
},
//富文字圖片上傳前
beforeUpload() {
// 顯示loading動畫
this.quillUpdateImg = true
},
//富文字圖片上傳成功
uploadSuccess(res, file) {
// res為圖片伺服器返回的資料
// 獲取富文字元件例項
console.log(res,file);
let quill = this.$refs.myQuillEditor.quill
// 如果上傳成功
if (res.code == 1000 ) {
// 獲取游標所在位置
let length = quill.getSelection().index;
// 插入圖片 res.url為伺服器返回的圖片地址
quill.insertEmbed(length, 'image', res.data)
// 調整游標到最後
quill.setSelection(length + 1)
} else {
this.$message.error('圖片插入失敗')
}
// loading動畫消失
this.quillUpdateImg = false
},
// 富文字圖片上傳失敗
uploadError() {
// loading動畫消失
this.quillUpdateImg = false
this.$message.error('圖片插入失敗')
},
}
}
複製程式碼
12. 使用cheerio展示字串表格
有一部分表格資料比較難處理,是後端直接把xlsx檔案轉成字串發給前端,cheerio可以把字串轉為類似jquery物件的虛擬DOM,然後用jquery的api操作這個虛擬DOM。
import cheerio from "cheerio"
this.$http.post('/api/list/statement-table',p).then(function(response){
if(response.data==""){
This.isShow=false;This.content=true;This.title=false//無資料時資料載入中和標題資料的盒子隱藏
This.message="<div style='text-align:center'>暫無資料</div>"
}else{
//console.log(response)
This.isShow=false;
This.content=true;//有資料時 資料載入中隱藏 標題和表體顯示
let $ = cheerio.load(response.data);
//刪除自帶的行內樣式
$("body style").remove();
$("body table").css({"border":"1px solid #e8eaec" });
$("body table td").css({"border":"1px solid #e8eaec","padding":"10px","color":"#515a6e"});
//全文匹配 剔除&quot;
This.message = $("body").html().replace(/&quot;/g,"");
}
})
複製程式碼
13. keep-live元件快取
產品的需求是從列表頁面點選檢視按鈕進入詳情頁面,詳情頁面再點選返回,列表頁面要不能重新整理,就需要把元件快取起來。
元件快取直接加keep-live
就可以了,比較麻煩的是我們在這個元件裡判斷三種情況,1.第一次進入 2.從其他欄目進入 3.從詳情頁進入,如果從為1、2這兩種情況,我們需要重新整理頁面,如果是3,則不重新整理。
思路是:
created
鉤子中著增加isFirstEnter
標識,beforeRouteEnter
鉤子中判斷是否為詳情頁面返回,如果是則加上meta.isBack
的標識,在activated
鉤子裡判斷是第幾種情況,如果為1或2,則重新請求列表頁資料,如果是3就不用動管了。
router.js
增加標識meta的keepAlive
和isBack
/******** 業務報表 Start ********/
{
path: '/Main/BusinessReport/:key', // 業務報表-列表
name: 'BusinessReport',
meta: { keepAlive: true,isBack:false},
component: () => import('./pages/BusinessReport/index.vue'),
},
{
path: '/Main/BusinessReportInfo/:sn/:is_check/:key/:type/:cmd5/:time/:is_down', // 業務報表-詳情
name: 'BusinessReportInfo',
component: () => import('./pages/BusinessReportInfo/index.vue'),
},
/******** 業務報表 End ********/
複製程式碼
根據mate.keepAlive
渲染不同的router-view
(忘記為什麼是這麼寫的了,感覺很low)。
<keep-alive>
<router-view v-if="$route.meta.keepAlive" ></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" >
<!-- 這裡是不被快取的檢視元件,比如 Edit! -->
</router-view>
複製程式碼
元件程式碼鉤子事件created
、beforeRouteEnter
、activated
方法
data(){
return{
isFirstEnter:false
}
},
created:function(){
this.isFirstEnter = true;
},
beforeRouteEnter(to, from, next) {
if(from.name === 'BusinessReportInfo') { //判斷是從哪個路由過來的,若是BusinessReportInfo頁面不需要重新整理獲取新資料,直接用之前快取的資料即可
to.meta.isBack = true
}
next();
},
activated() {
if(!this.$route.meta.isBack || this.isFirstEnter) {
this.data=""
//如果isBack是false,表明需要獲取新資料,否則就不再請求,直接使用快取的資料
this.getPath(); // ajax獲取資料方法
}
this.$route.meta.isBack = false;
this.isFirstEnter=false;
//恢復成預設的false,避免isBack一直是true,導致下次無法獲取資料
}
複製程式碼
14. 不要在子元件中操作父元件的資料
確實可以在子元件中修改父元件的資料,但強烈建議不要在子元件中操作父元件資料,期間我接手過一個功能,梳理了半天邏輯,沒找到觸發點在哪裡,原來是在子元件中操作了父元件的資料,不利於維護,我自己起了個名字,讓資料保持單向流動,不知道是不是可以定義為單項資料了原則?。
在開發的過程中我們發現,每個人寫的業務元件程式碼風格都不一致,怎樣是一致,關於業務元件,有沒有好的規範或者原則呢?還希望大家給點資料和建議非常感謝。