目錄css
├── build ├── config ├── dist //打包後生成的靜態頁面文件 ├── src │ ├── assets //全局靜態圖片 │ ├── components //全部組件(項目核心部分) │ │ ├── element //業務代碼 │ │ ├── layout //佈局代碼(導航,頭,和主體部分) │ │ ├── login //登陸代碼 │ │ ├── public//公用功能組件 │ ├── filters //全局過濾器 │ ├── my-theme //全局樣式表 │ ├── router //路由 │ ├── sever │ │ ├── api.js //全局API │ │ ├── method // API方法 │ │ ├── https.js // axios封裝,路由請求響應攔截等 │ ├── store //狀態管理 │ ├── utils //公用方法 │ │ ├── public_function //公用api增刪改查審覈做廢等方法 │ │ ├── fetch.js //簽名生成方法 │ │ ├── rules.js //封裝驗證方法 │ ├── App.vue //父組件 │ ├── main.js //入口文件 └── static //靜態資源
項目目錄是採用 vue-cli 自動生成的,如需添加依賴,"$ npm install 依賴名" 便可(若是僅用於開發階段,則使用 $ npm install --save-dev )。html
注:-save 和 save-dev 能夠省掉你手動修改 package.json 文件的步驟。前端
npm install module-name -save 自動把模塊和版本號添加到 dependencies 部分。
npm install module-name -save-dev 自動把模塊和版本號添加到 devdependencies 部分。vue
現已將 vue-cli2 升級到 vue-cli3 ,大大加快了項目編譯及打包速度, 但 src 下的目錄結構與業務代碼幾乎沒有變更。
ios
頁面使用方法git
/** 1. 在 created 或者 mounted 鉤子函數初始化時獲取 2. 在template模板中給按鈕綁定權限 **/ data(){ return { authority: {}, } }, created() { this.authority = JSON.parse(localStorage.getItem("authority")); }
<!-- template --> <el-button icon="el-icon-circle-plus-outline" @click="salesAddF" v-if="authority.b_add == 1">新增</el-button> <el-button icon="el-icon-delete" @click="deleteF" v-if="authority.b_del == 1">刪除</el-button>
// router/index.js import Vue from "vue"; import axios from "@/sever/https"; import Router from "vue-router"; Vue.use(Router); const router = new Router({ base: '/tomcat/', routes: [{ path: "/login", name: "登陸頁面", component: login, }, ...], ] }); router.beforeEach((to, from, next) => { var objArr = str.split("/", 3); if (to.matched.some(r => r.meta.requireAuth)) { if (store.state.system_app.token) { //登陸狀態當監測到路由變化,則去請求進入頁面的權限接口 axios({ method: "get", url: "xxxRuleApiUrl", params: { code: objArr[2] } }).then(res => { if (res.data.code == 200) { if (res.data.data.is_role == 'Y') { //若是該頁面設有權限則將權限詳情先存於本地 window.localStorage.removeItem("authority") let authority = JSON.stringify(res.data.data.role_list); localStorage.authority = authority; next(); } else { // 未有頁面訪問權限 next({ path: "/isRole", query: { redirect: to.fullPath } }); } next(); } }); } else { next({ path: "/login", query: { redirect: to.fullPath } }); } } else { next(); } }); export default router;
例,使用vuex存儲變量,切換新增修改查看頁面github
頁面調用方法ajax
// 使用方法:調用 ACTION 的salesorderIndexACT方法,改變modulename爲Index, // 切換組件到首頁並更新視圖 // salesorderAdd.vue import { mapActions, mapGetters } from "vuex"; methods:{ ...mapActions([ "salesorderAdd", "salesorderIndexACT" ]), goBack() { // 回到模塊首頁,使用 ACTION salesorderIndexACT this.salesorderIndexACT({ moduleName: "Index" }); } }
初始化頁面算法
<!-- template 中默認綁定列表頁 --> <template> <div> <component v-bind:is="commont"></component> </div> </template>
頁面中間件vue-router
//組件中引入各模塊 <script> import Index from "@/components/element/sales/salesordermanage/salesorder/salesorderIndex"; import salesorderAdd from "@/components/element/sales/salesordermanage/salesorder/salesorderAdd"; import salesorderSee from "@/components/element/sales/salesordermanage/salesorder/salesorderSee"; import { mapGetters } from "vuex"; export default { data() { return { commont: "Index" }; }, components: { Index, //初始化首頁 salesorderAdd, //新增頁 }, computed: { ...mapGetters([ "salesorderIndexGet", "salesorderAddGet", "salesorderSeeGet" ]) }, watch: { salesorderIndexGet() { this.commont = this.salesorderIndexGet.moduleName; }, salesorderAddGet() { this.commont = this.salesorderAddGet.moduleName; }, salesorderSeeGet() { this.commont = this.salesorderSeeGet.moduleName; } } }; </script>
定義狀態的方法
//state.js export default { // 銷售訂單 salesorderIndex: {}, salesorderAdd: {}, salesorderSee: {}, ... }; /** * 最好不要直接使用,state主要用於存取變量,例如: ...mapState({ aa: state => state.system_app.user, }) **/ // ============= 分割線 ============= // //getter.js export default { // 銷售訂單 salesorderIndexGet(state) { return state.salesorderIndex; }, salesorderAddGet(state) { return state.salesorderAdd; }, salesorderSeeGet(state) { return state.salesorderSee; } }; /** * 用法:寫到watch或者computed中,ShowCon相似於data中的一個對象,getShowCon是getters中的一個方法,例如: ...mapGetters({ ShowCon:"getShowCon" }) **/ // ============= 分割線 ============= // //mutations.js export default { // 銷售訂單 salesorderIndex(state, info) { state.salesorderIndex = info }, salesorderSee(state, info) { state.salesorderSee = info }, salesorderAdd(state, info) { state.salesorderAdd = info } } /** * 獲取方法,clickF是本地調用的方法,@click="clickF(...arg)",salesorderIndex是寫到mutations裏邊的方法,例如: ...mapMutations({ clickF:"salesorderIndex" }) **/ // ============= 分割線 ============= // //actions.js export default { salesorderIndexACT(context, data) { context.commit("salesorderIndex", data); }, salesorderSee(context, data) { context.commit("salesorderSee", data); }, salesorderAdd(context, data) { context.commit("salesorderAdd", data); } }; /** * 執行或者多個mutations的方法 **/
頁面上使用
<template slot-scope="scope"> {{scope.row.price | priceFormat}} </template>
引入寫好的過濾器模塊,遍歷全部過濾器並掛載到Vue實例上,組件裏便可以直接使用
// main.js import filters from '@/filters/index.js' Object.keys(filters).forEach(key => { Vue.filter(key, filters[key]) });
自定義過濾器
//filter.js export default { //價格保留兩位小數並使用千分位分隔符 priceFormat(s, n) { //能夠小於0 if (!s && s !== 0) { return s } var numberS = Number(s) if (numberS) { n = n > 0 && n <= 20 ? n : 2; numberS = parseFloat((s + "").replace(/[^\d\.-]/g, "")).toFixed(n) + ""; var l = numberS.split(".")[0].split("").reverse(), r = numberS.split(".")[1]; var t = ""; for (var i = 0; i < l.length; i++) { t += l[i] + ((i + 1) % 3 == 0 && (i + 1) != l.length ? "," : ""); } if (s < 0 && Math.abs(s) < 1000) { return s } else { return t.split("").reverse().join("") + "." + r; } } else { return s } }, ... };
避免首次加載頁面的時候加載整個模塊代碼(致使幾秒白屏),加入了路由懶加載
//router/index.js { path: "xx", name: "xx", component: resolve => require([ "@/components/element/xx/xx/xx.vue" ], resolve),//路由懶加載 meta: { _meauname: '自定義標題' } }
可加入默認自定義參數
// sever/https.js axios.defaults.timeout = 10000; //可等待時長10s axios.defaults.baseURL = "http://60.205.214.105"; //接口baseURL axios.defaults.headers.post["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";//請求默認頭信息
在響應攔截 axios.interceptors.response.use 裏若是接口報錯,根據狀態碼給出相應提示語和項目公用方法規範
import axios from "axios"; import { Message, Loading } from "element-ui"; // 請求攔截器 axios.interceptors.request.use( config => { loading = Loading.service({ fullscreen: true, lock: true, text: '正在加載,請稍等……' }); if (config.method == "post") { removePending(config); //在一個ajax發送前執行一下取消操做 config.cancelToken = new cancelToken((c) => { // 這裏的ajax標識我是用請求地址&請求方式拼接的字符串,固然你能夠選擇其餘的一些方式 pending.push({ u: config.url + '&' + config.method, f: c }); }); let postData = config.data; config.data.sign = serverMoudle(postData); } else if (config.method == "get") { let getData = config.params; config.params = { sign: serverMoudle(getData), ...config.params }; } if (store.state.system_app.token) { config.headers.Token = `${store.state.system_app.token}`; } // 每次請求添加sign return config; }, err => { if (loading) { loading.close(); } Message({ showClose: true, message: err.msg, type: 'error' }) return Promise.reject(err); } ); // 響應攔截器 axios.interceptors.response.use( response => { if (loading) { loading.close(); } const data = response.data; if (response.config.method == 'post') { removePending(response.config); } if (data.code == 401) { store.dispatch('LOGOUT') router.replace({ path: "/login" }); } else if (data.code == 400) { Message.error(data.msg) } else if (data.code == 503) { Message.error(data.msg) store.dispatch('LOGOUT') router.replace({ path: "/login" }); } return response; }, error => { Message({ message: "網絡錯誤", type: "error", duration: 5 * 1000 }); return Promise.reject(error); } ); export default axios;
將查詢、重置、切換頁碼、切換列表長度四個功能封裝成爲一個公用的方法。這四者有着密切的關係,又有各自的不一樣點(默認:列表30項,顯示第一頁),它們的傳遞值分別爲:
功能 | params(搜索參數) | page(頁碼) | per_page(分頁長度) |
---|---|---|---|
查詢 | 已填搜索項 | 1 | 已選的分頁長度 |
重置 | 搜索項置空(根據需求) | 1 | 30 |
切換頁碼 | 已填搜索項 | 已選的頁碼 | 已選的分頁長度 |
切換分頁長度 | 已填搜索項 | 已選的頁碼 | 已選的分頁長度 |
使用方法
import { getData } from "@/utils/public_function/commonUtils";//初始化getData方法參數 this.$set(this, "params", { defaultParams: "xx" }); this.$set(this, "searchParams", { params: this.params, //默認參數 TableData: this.TableData, //列表數據 API: "getList", //列表接口名 vm: this //Vue }); getData(this.searchParams); //獲取列表 //在實現相應功能時,能夠直接使用組合方法改變this.searParams.params的值,便可。 onSearch(){ Object.assign(this.searchParams.params, { per_page: val,//分頁長度 page: 1//第一頁 }); getData(this.searchParams); //獲取列表 }
getData 方法
//commonUtils.js export function getData(arg, type) { /* * @Date: 2018-07-06 10:23:10 * 初始化 搜索,重置,切換頁碼,切換頁碼長度 都須要調取該方法從新初始化列表 * arg格式: * { * params:{},//請求參數 TableData: this.TableData, //列表數據 API: "getxxx", //接口名 vm: this //傳遞的this } */ if (arg["params"]) { arg["vm"]["goPage"] = "page" in arg["params"] ? arg["params"]["page"] : 1; //改變當前頁 arg["vm"]["pageSize"] = "per_page" in arg["params"] ? arg["params"]["per_page"] : 30; //改變當前頁size } arg.vm.loading = true; axios({ method: "get", url: api[arg.API], params: arg.params }).then(res => { if ([200, 304].includes(res.data.code)) { arg.TableData.tableData = res.data.data.data; arg.TableData.total = Number(res.data.data.total); arg.TableData.allList = res.data.data.desc; } arg.vm.loading = false; }); }
需求:
//使用方法 import { getCompanyListCommon, getDepartmentListCommon, getEmployeeListCommon } from "@/utils/public_function/commonUtils"; //引入 獲取組織、部門、業務員 方法 getCompanyListCommon(this); //便可獲取組織 其餘同理。其中三者的聯動可在各自的回調完成以後再調取下一級數據
使用方法
import { bindIdsF } from "@/utils/public_function/commonUtils"; //在詳情接口回調中,綁定 this.$axios({ method: "get", url: xxx, params: { order_id: xxx } }).then(({ data }) => { if (data.code == 200) { /** * data.data:下拉綁定的字段可能和詳情接口返回的字段不同,須要在這裏用assign作下處理,多數狀況下不須要 * assign方法以下: */ Object.assign(data.data, { employee_id: data.data.business_id, employee_name: data.data.business_name }); // 列表下有 options 則傳入,沒有可不傳入 bindIdsF(this, data.data, [ ["customer_id", "customer_name", "customerList", "options"], ["department_id", "department_name", "departmentList", "options"], ["employee_id", "employee_name", "employeeList", "options"] ]); } });
綁定下拉框數據方法
// commonUtils.js // 初始化綁定返回的id 下拉 export function bindIdsF(self, oParent, arrList) { if (Array.isArray(arrList)) { for (const item of arrList) { let arr = []; if (item[3]) { arr = [{ [item[0]]: Number(oParent[item[0]]), [item[1]]: oParent[item[1]], }]; self.$set(self[item[2]], item[3], arr) } else { arr = [{ [item[0]]: Number(oParent[item[0]]), [item[1]]: oParent[item[1]], }]; self.$set(self, item[2], arr) } } } }
注意:遇到特殊狀況(如選客戶等帶出其餘下拉字段(業務員等)),需在相應接口回調裏繼續使用 bindIdsF 去初始化下拉數據
驗證模塊(utils/rules.js)中定義了組件上全部用到的驗證,而後將rules 掛載的Vue原型上便可。
使用方法
/** * 組件中直接調用rules下的對應方法便可 * 若是僅加星號,不作限制,只用prop屬性便可 **/ <el-form :model="param" ref="paramsRef"> <el-form-item label="庫存組織" prop="address" :rules="rules.address"> <el-input v-model="params.address"></el-input> </el-form-item> </el-form>
將驗證方法掛載到vue實例上
// main.js import rules from './utils/rules' //全局定義表單驗證規則 Vue.prototype.rules = rules;
rules.js 頁面定義方法,經常使用的有:
// 自定義方法,做爲 validator 的屬性值 // 第一位不能爲0,保留兩位小數 const checkNumPot2 = (rule, value, callback) => { const reg = /(^[0-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/; if (!value && value !== 0) { return callback(new Error("請填寫數字")); } else if (!reg.test(value)) { return callback(new Error("請填寫數字,最多2位小數")); } else { callback(); } }; // 驗證身份證 const checkIdNum = (rule, value, callback) => { const reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/; if (!reg.test(value)) { return callback(new Error("證件號碼不正確")); } else { callback(); } }; // 驗證整數 const checkInterNum = (rule, value, callback) => { const reg = /^[0-9]*[1-9][0-9]*$/; if (!value) { return callback(new Error("請填寫整數")); } else if (!reg.test(value)) { return callback(new Error("請輸入整數")); } else { callback(); } }; //驗證座機 const telephone = (rule, value, callback) => { const reg = /^([0-9]{3,4}-)?[0-9]{7,8}$/; if (!reg.test(value) && value != undefined && value != "") { return callback(new Error('請正確輸入固定電話,格式爲"區號-座機號碼"')); } else { callback(); } }; //輸入不可爲空 const empty = (rule, value, callback) => { if (value == "") { return callback(new Error("輸入不能爲空")); } else { callback(); } }; //郵箱驗證 const emailNew = (rule, value, callback) => { const reg = /^[A-Za-z\d]+([-_.][A-Za-z\d]+)*@([A-Za-z\d]+[-.])+[A-Za-z\d]{2,5}$/; if (!reg.test(value)) { return callback(new Error('4~18個字符,可以使用字母、數字、下劃線,需以字母開頭@xxx.com/cn')); } else { callback(); } }; /** * requited: 是否可爲空 * pattern:自定義正則 * validator:驗證方法 * message:驗證不經過消息 * trigger:觸發方式 * * pattern 和 validator 寫一個便可 **/ export default { validatePhone: [{ required: true, pattern:: /^1[0123456789]\d{9}$/, message: "目前只支持中國大陸的手機號碼", trigger: "blur" }], validateEmpty: [{ required: true, validator: empty, trigger: "blur" }], //不可輸入爲空 numPot2: [{ required: true, validator: checkNumPot2, trigger: "blur" }], // 第一位不能爲0,保留兩位小數 interNum: [{ required: true, validator: checkInterNum, trigger: "blur" }], // 整數 telephone: [{ required: false, validator: telephone, trigger: "blur" }], // 驗證座機 emailNew: [{ required: false, validator:emailNew, message: "請輸入正確的郵箱格式:xxx@xxx.com/cn", trigger: "blur" }], company_id: [{ required: true, message: "請選擇組織", trigger: "change" }], ... };
自定義打印模板後,以組件的形式引入並調用其打印方法便可。
打印官網
使用方法
// template <el-button type="text" @click="printF(scope, scope.row)"> 打印揀貨單 </el-button> <!-- 打印組件 --> <div> <printPage ref="printRef"></printPage> </div> <script> import printPage from "@/components/public/pickingList.vue"; //打印組件 // 觸發打印組件 methods:{ printF(scope, row) { this.$nextTick(function() { this.$refs.printRef.printPdf(row.outstock_id); }); }, } </script>
打印內容頁面(自定義的格式)
// 引用打印插件:@/utils/LodopFuncs.js // pickingList.vue <template> <div v-show="false"> <div v-if="params!=null"> <div id="print-header"> <table class="header" cellpadding="0" cellspacing="0" style="width: 100%;"> <tr> <td> 表頭 </td> </tr> </table> </div> <div id="print-body"> <table class="body" cellpadding="0" cellspacing="0" style="width: 100%;"> <tr v-for="item in tableData" :key="item.id"> <td>表體</td> </tr> </table> </div> <div id="print-footer"> <table class="footer" cellpadding="0" cellspacing="0" style="width: 100%;"> <tr> <td> 頁腳 </td> </tr> </table> </div> <div id="print-pages"> <p style='text-align:center;font-size:9pt;'> 頁碼: <span tdata='pageNO'>第 ## 頁</span> / <span tdata='pageCount'>共 ## 頁</span> </p> </div> </div> </div> </template> <script> import { getLodop } from "@/utils/LodopFuncs"; let LODOP = getLodop; export default { data() { return { params: null, tableData: null }; }, methods: { list(val) { let vm = this; this.$axios({ method: "get", url: this.$api.printPick, params: { outstock_id: val } }).then(res => { if (res.data.code == 200) { vm.params = res.data.data; vm.tableData = res.data.data.goods_info; setTimeout(function() { vm.CreatePrintPages(); }, 0); } }); }, printPdf(val) { this.list(val); }, CreatePrintPages() { var LODOP = getLodop(); if (!LODOP) { return false; } var strStyle = LODOP.strStyle; LODOP.PRINT_INIT(""); LODOP.SET_PRINT_PAGESIZE(1, "205mm", "93.3mm", "CreateCustomPage"); // 0 操做者自行決定或打印機缺省設置 1 縱向打印,固定紙張;2 橫向打印,固定紙張 LODOP.SET_PREVIEW_WINDOW(1, 0, 0, 1000, 600, ""); // 初始預覽窗口大小 LODOP.SET_SHOW_MODE("LANDSCAPE_DEFROTATED", 1); // 橫向打印時正向顯示 LODOP.SET_SHOW_MODE("HIDE_PAPER_BOARD", 1); // 去除背景滾動線 LODOP.SET_PRINT_MODE("AUTO_CLOSE_PREWINDOW", 1); // 打印後自動關閉預覽 LODOP.SET_PRINT_MODE("CUSTOM_TASK_NAME", "揀貨單"); // 打印隊列中的文檔名 // 追加打印頭部 LODOP.ADD_PRINT_TABLE( "2mm", "10mm", "185mm", "30mm", strStyle + document.getElementById("print-header").innerHTML ); LODOP.SET_PRINT_STYLEA(0, "ItemType", 1); // 頁眉頁腳項 // 追加打印主體:分頁、循環表格 LODOP.ADD_PRINT_TABLE( "28mm", "10mm", "185mm", "20mm", strStyle + document.getElementById("print-body").innerHTML ); // 追加打印底部 LODOP.ADD_PRINT_TABLE( "63mm", "10mm", "185mm", "30mm", strStyle + document.getElementById("print-footer").innerHTML ); LODOP.SET_PRINT_STYLEA(0, "ItemType", 1); // 頁眉頁腳項 // 追加頁碼 LODOP.ADD_PRINT_HTM( "80mm", "10mm", "185mm", "5mm", document.getElementById("print-pages").innerHTML ); LODOP.SET_PRINT_STYLEA(0, "ItemType", 1); LODOP.PREVIEW(); } } }; </script>
簽名算法先後臺要保持一致
使用
//簽名使用方法 在 axios 請求攔截中,給發送的數據用簽名算法格式化 config.data.sign = serverMoudle(config.data) //post請求 config.params.sign = serverMoudle(config.params) //get請求
簽名算法
import md5 from 'js-md5'; import Qs from "qs"; const unchangeable = 'xxxxxxx'; //根據該字符串加密 import { objKeySort } from '@/utils/public_function/commonUtils.js'; //將接口參數排序並將null或undefined的數據設爲空 export function objKeySort(obj) { var newkey = Object.keys(obj).sort(); var newObj = {}; for (var i = 0; i < newkey.length; i++) { //過濾參數中null或者undefined的值,並使之默認爲空,不然報錯 if ([null, undefined].includes(obj[newkey[i]])) obj[newkey[i]] = ''; newObj[newkey[i]] = obj[newkey[i]]; } return newObj; } export function serverMoudle(params) { let newUrl = '' if (params == undefined) { newUrl = "key=" + unchangeable; return md5(newUrl) } else { if (Qs.stringify(objKeySort(params)) != '') { newUrl = decodeURIComponent(Qs.stringify(objKeySort(params))) + "&key=" + unchangeable; } else { newUrl = "key=" + unchangeable; } return md5(newUrl) } }
1.直接只用對象、數組字面量初始化變量,不用new它的實例(官方推薦,簡單明瞭)
let arr = [] , obj = {};
2.儘可能用全等判斷是否相等(===);
null === false; //false
3.用變量自己的布爾類型去作判斷,必須轉布爾值時可使用雙感嘆號轉布爾類型。
!!null === false //true
4.用其餘代碼優雅替換if else語句(三目運算符、switch、邏輯判斷等)
5.寫有意義的註釋,瞭解一些特殊標記,e.g.
TODO: 有功能待實現。
FIXME: 該處代碼運行沒問題,但可能因爲時間趕或者其餘緣由,須要修正。
HACK: 爲修正某些問題而寫的不太好或者使用了某些詭異手段的代碼。
XXX: 該處存在陷阱。
6.避免代碼冗餘,能循環則不去單個操做變量
1. 過濾器:
2. 父子組件相互傳遞數據:
3. 樣式:
4. data
5. $set
6. JSX語法
如需渲染較爲複雜的 DOM ,可以使用Jsx語法簡化代碼 GITHUB 點擊
7. 儘可能使用Vue自帶API
8. 性能優化:
組件名規範:
新增頁面:xxxAdd.vue / 修改頁面:xxxModify.vue / 主頁面:xxxIndex.vue / 查看頁面:xxxSee.vue
方法名規範:
查詢:onSearch / 重置:onReset / 修改某個功能:changeXxx / 新增:addF / 修改:modifyF / 查看:seeF
變量名規範:
列表數據:xxxList / 表格數據:TableData.tableData / 表頭參數:params / 默認值:defaultXxx
狀態管理變量名規範:
states:xxxIndex
mutations:xxxIndex
getters:xxxIndexGet
actions:xxxIndexACT
縮進:兩個空格
去除eslint語法檢測,雖然能使代碼更規範,可是及其影響工做效率,不推薦。
建議使用編輯器vscode或sublime Text,學會利用編輯器的有用插件。
插件推薦:
① 提升效率:HTML Snippets(html snippets)、Javascript (ES6) Code Snippets(ES6 snippets)、Auto Close Tag(自動補全)、Emmet(css snippets)、Vue2 Snippets
② 功能加強:Color Highlight(識別代碼的顏色)、Bracket Pair
Colorizer(識別代碼中的括號,標記不一樣的顏色)
③ 設置編輯器自帶的自動保存、自動格式化代碼功能
④ 等等
該項目還在不斷的完善當中,避免不了存在着一些不足,如下是目前規劃的開發計劃。