看了官方的小程序開發指南,作了個稅計算器來練手。
效果以下:git
項目結構以下:github
1、簡介
一、頁面的樣式主要使用了官方WeUI。
WeUI 是一套同微信原生視覺體驗一致的基礎樣式庫,下載地址https://github.com/Tencent/weui-wxss。
使用方法:
(1)新建一個目錄style,把下載的WeUI源碼裏面的weui.wxss放進去;
(2)app.wxss中導入@import 'style/weui.wxss';
二、表單驗證用了WxValidate.js
用法也比較簡單:
(1)wxml中
錄入框設置value值
<input type="digit" class="weui-input" name="beforeTax" value='{{form.beforeTax}}'/>
form綁定提交方法
<form bindsubmit='formSubmit'>
(2)js中
onLoad函數中初始化驗證表單字段this.initValidate();
formSubmit函數調用驗證函數
const params = e.detail.value
if (!this.WxValidate.checkForm(params)) {
const error = this.WxValidate.errorList[0]
this.showMessage(error)
return false
}
三、表格的實現
小程序中沒有像網頁的table表格控件,搜索了網上,用了view實現表格效果。
有一個注意的地方是網頁table只須要設置第一行的每列寬度就行,但用view的話須要每行每列都設置寬度,不然每行內容長度
不一致時就錯亂了。小程序
2、主要文件源碼緩存
一、index.wxml微信
<form bindsubmit='formSubmit'> <view class="weui-cells weui-cells_after-title"> <view class="weui-cell weui-cell_input"> <view class="weui-cell__hd"> <view class="weui-label">稅前工資</view> </view> <view class="weui-cell__bd"> <input type="digit" class="weui-input" name="beforeTax" value='{{form.beforeTax}}'/> </view> <view class="weui-cell__ft">元</view> </view> <view class="weui-cell weui-cell_input"> <view class="weui-cell__hd"> <view class="weui-label">五險一金</view> </view> <view class="weui-cell__bd"> <input type="digit" class="weui-input" name="insurance" value="{{form.insurance}}"/> </view> <view class="weui-cell__ft">元</view> </view> <view class="weui-cell weui-cell_input"> <view class="weui-cell__hd"> <view class="weui-label">附加扣除</view> </view> <view class="weui-cell__bd"> <input type="number" class="weui-input" name="otherDeduct" value="{{form.otherDeduct}}"/> </view> <view class="weui-cell__ft">元</view> </view> <view class="weui-cell weui-cell_input"> <view class="weui-cell__hd"> <view class="weui-label">個稅起徵</view> </view> <view class="weui-cell__bd"> <input class="weui-input" name="threshold" disabled='disabled' value="5000"/> </view> <view class="weui-cell__ft">元</view> </view> <view class="weui-btn-area"> <button class="weui-btn green-btn" hover-class="green-hover-class" form-type="submit" >計算</button> </view> </view> <view class="weui-cells__title">計算結果</view> <view class="weui-cell weui-cell_input"> <view class="weui-cell__hd"> <view class="weui-label">應納所得</view> </view> <view class="weui-cell__bd"> {{ resAfterDeduct }} </view> <view class="weui-cell__ft">元</view> </view> <view class="weui-cell weui-cell_input"> <view class="weui-cell__hd"> <view class="weui-label">適用稅率</view> </view> <view class="weui-cell__bd"> {{ resTaxRate }} </view> <view class="weui-cell__ft">%</view> </view> <view class="weui-cell weui-cell_input"> <view class="weui-cell__hd"> <view class="weui-label">速算扣除</view> </view> <view class="weui-cell__bd"> {{ resQuickDeduct }} </view> <view class="weui-cell__ft">元</view> </view> <view class="weui-cell weui-cell_input"> <view class="weui-cell__hd"> <view class="weui-label">應繳稅款</view> </view> <view class="weui-cell__bd"> {{ resTax }} </view> <view class="weui-cell__ft">元</view> </view> <view class="weui-cell weui-cell_input"> <view class="weui-cell__hd"> <view class="weui-label">稅後工資</view> </view> <view class="weui-cell__bd"> {{ resAfterTax }} </view> <view class="weui-cell__ft">元</view> </view> <view class="table"> <view class="tr"> <view class="th">我的所得稅稅率表</view> </view> <view class="tr"> <view class="th th1">級數</view> <view class="th th2">應納稅所得額</view> <view class="th th3">稅率(%)</view> <view class="th th4">速算扣除數</view> </view> <block wx:for="{{taxArray}}" wx:key="level"> <view class='tr {{ item.level % 2 == 0 ? "rowColor":"" }}'> <view class="td th1">{{item.level}}</view> <view class="td th2">{{item.name}}</view> <view class="td th3">{{item.rate}}</view> <view class="td th4">{{item.deduct}}</view> </view> </block> </view> <view class="weui-footer weui-footer_fixed-bottom"> <view class="weui-footer__text">Copyright © 2019 jlc</view> </view> </form>
二、index.wxssapp
.green-btn{ background-color: #a6a8aa; color:#fff; } .green-hover-class{ background-color: #737577; color:#fff; } .table { border: 1px solid #ccc; border-right: 0; border-bottom: 0; width: 100%; margin-top: 20px; margin-bottom: 50px; } .tr { width: 100%; display: flex; justify-content: space-between; } .th, .td { padding: 5px; border-bottom: 1px solid #c2bdbd; border-right: 1px solid #c2bdbd; text-align: center; width: 100%; font-size:12px; } .th { font-weight: 400; background-color: #dadada; } .th1{ width:9%; } .th2{ width:55%; } .th3{ width:15%; } .th4{ width:21%; } .rowColor{ background-color: #dadada; }
三、index.jsxss
import WxValidate from '../../utils/WxValidate.js' Page({ /** * 頁面的初始數據 */ data: { showTopTips: false, form:{ beforeTax: 10000,//稅前 insurance: 0,//五險一金 otherDeduct: 0,//附加扣除 threshold: 5000//起徵點 }, taxArray: [ { level: 1, name: '未超過3000元的部分', minTax: 0,rate:3, deduct:0 }, { level: 2, name: '超過3000至12000元的部分', minTax: 3000,rate: 10, deduct: 210 }, { level: 3, name: '超過12000至25000元的部分', minTax: 12000,rate: 20, deduct: 1410 }, { level: 4, name: '超過25000至35000元的部分', minTax: 25000,rate: 25, deduct: 2660 }, { level: 5, name: '超過35000至55000元的部分', minTax: 35000,rate: 30, deduct: 4410 }, { level: 6, name: '超過55000至80000元的部分', minTax: 55000,rate: 35, deduct: 7160 }, { level: 7, name: '超過80000元的部分', minTax: 80000, rate: 45, deduct: 15160 } ] }, calTax: function () { }, /** * 生命週期函數--監聽頁面加載 */ onLoad: function (options) { //初始化表單 this.initValidate(); }, //驗證表單字段 initValidate() { const rules = { beforeTax: { required: true, number: true }, insurance: { required: true, number: true }, otherDeduct: { required: true, digits: true } } const messages = { beforeTax: { required: '請填寫稅前工資', number: '請輸入正確格式的稅前工資' }, insurance: { required: '請填寫五險一金', number: '請填寫正確格式的五險一金' }, otherDeduct: { required: '請填寫附加扣除', digits: '附加扣除只能爲整數' } } this.WxValidate = new WxValidate(rules, messages) }, //報錯 showMessage(error) { /*wx.showModal({ content: error.msg, showCancel: false, })*/ wx.showToast({ title: error.msg, icon: 'none', duration: 3000 }); }, //調用驗證函數 formSubmit: function (e) { console.log('form發生了submit事件,攜帶的數據爲:', e.detail.value) const params = e.detail.value //校驗表單 if (!this.WxValidate.checkForm(params)) { const error = this.WxValidate.errorList[0] this.showMessage(error) return false } var resAfterDeduct = 0;//應納所得 var resTaxRate = 0;//稅率 var resQuickDeduct = 0;//速算扣除 var resTax = 0;//應繳稅款 var resAfterTax = 0;//實發工資 resAfterDeduct = params.beforeTax - params.insurance - params.otherDeduct - params.threshold; if (resAfterDeduct < 0){ resAfterDeduct = 0; }else{ var arr = this.data.taxArray; var arrLength = arr.length; for (var i = arrLength - 1; i >= 0; i--) { if (resAfterDeduct > arr[i].minTax) { resTaxRate = arr[i].rate; resQuickDeduct = arr[i].deduct; break; } } resTax = resAfterDeduct * (resTaxRate / 100) - resQuickDeduct; resAfterTax = params.beforeTax - params.insurance - resTax; } this.setData({ resAfterDeduct: resAfterDeduct, resTaxRate: resTaxRate, resQuickDeduct: resQuickDeduct, resTax: resTax, resAfterTax: resAfterTax }) }, /** * 生命週期函數--監聽頁面初次渲染完成 */ onReady: function () { }, /** * 生命週期函數--監聽頁面顯示 */ onShow: function () { }, /** * 生命週期函數--監聽頁面隱藏 */ onHide: function () { }, /** * 生命週期函數--監聽頁面卸載 */ onUnload: function () { }, /** * 頁面相關事件處理函數--監聽用戶下拉動做 */ onPullDownRefresh: function () { }, /** * 頁面上拉觸底事件的處理函數 */ onReachBottom: function () { }, /** * 用戶點擊右上角分享 */ onShareAppMessage: function () { } })
四、WxValidate.jside
/** * 表單驗證 * * @param {Object} rules 驗證字段的規則 * @param {Object} messages 驗證字段的提示信息 * */ class WxValidate { constructor(rules = {}, messages = {}) { Object.assign(this, { data: {}, rules, messages, }) this.__init() } /** * __init */ __init() { this.__initMethods() this.__initDefaults() this.__initData() } /** * 初始化數據 */ __initData() { this.form = {} this.errorList = [] } /** * 初始化默認提示信息 */ __initDefaults() { this.defaults = { messages: { required: '這是必填字段。', email: '請輸入有效的電子郵件地址。', tel: '請輸入11位的手機號碼。', url: '請輸入有效的網址。', date: '請輸入有效的日期。', dateISO: '請輸入有效的日期(ISO),例如:2009-06-23,1998/01/22。', number: '請輸入有效的數字。', digits: '只能輸入數字。', idcard: '請輸入18位的有效身份證。', equalTo: this.formatTpl('輸入值必須和 {0} 相同。'), contains: this.formatTpl('輸入值必須包含 {0}。'), minlength: this.formatTpl('最少要輸入 {0} 個字符。'), maxlength: this.formatTpl('最多能夠輸入 {0} 個字符。'), rangelength: this.formatTpl('請輸入長度在 {0} 到 {1} 之間的字符。'), min: this.formatTpl('請輸入不小於 {0} 的數值。'), max: this.formatTpl('請輸入不大於 {0} 的數值。'), range: this.formatTpl('請輸入範圍在 {0} 到 {1} 之間的數值。'), } } } /** * 初始化默認驗證方法 */ __initMethods() { const that = this that.methods = { /** * 驗證必填元素 */ required(value, param) { if (!that.depend(param)) { return 'dependency-mismatch' } else if (typeof value === 'number') { value = value.toString() } else if (typeof value === 'boolean') { return !0 } return value.length > 0 }, /** * 驗證電子郵箱格式 */ email(value) { return that.optional(value) || /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(value) }, /** * 驗證手機格式 */ tel(value) { return that.optional(value) || /^1[34578]\d{9}$/.test(value) }, /** * 驗證URL格式 */ url(value) { return that.optional(value) || /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(value) }, /** * 驗證日期格式 */ date(value) { return that.optional(value) || !/Invalid|NaN/.test(new Date(value).toString()) }, /** * 驗證ISO類型的日期格式 */ dateISO(value) { return that.optional(value) || /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(value) }, /** * 驗證十進制數字 */ number(value) { return that.optional(value) || /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value) }, /** * 驗證整數 */ digits(value) { return that.optional(value) || /^\d+$/.test(value) }, /** * 驗證身份證號碼 */ idcard(value) { return that.optional(value) || /^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(value) }, /** * 驗證兩個輸入框的內容是否相同 */ equalTo(value, param) { return that.optional(value) || value === that.data[param] }, /** * 驗證是否包含某個值 */ contains(value, param) { return that.optional(value) || value.indexOf(param) >= 0 }, /** * 驗證最小長度 */ minlength(value, param) { return that.optional(value) || value.length >= param }, /** * 驗證最大長度 */ maxlength(value, param) { return that.optional(value) || value.length <= param }, /** * 驗證一個長度範圍[min, max] */ rangelength(value, param) { return that.optional(value) || (value.length >= param[0] && value.length <= param[1]) }, /** * 驗證最小值 */ min(value, param) { return that.optional(value) || value >= param }, /** * 驗證最大值 */ max(value, param) { return that.optional(value) || value <= param }, /** * 驗證一個值範圍[min, max] */ range(value, param) { return that.optional(value) || (value >= param[0] && value <= param[1]) }, } } /** * 添加自定義驗證方法 * @param {String} name 方法名 * @param {Function} method 函數體,接收兩個參數(value, param),value表示元素的值,param表示參數 * @param {String} message 提示信息 */ addMethod(name, method, message) { this.methods[name] = method this.defaults.messages[name] = message !== undefined ? message : this.defaults.messages[name] } /** * 判斷驗證方法是否存在 */ isValidMethod(value) { let methods = [] for (let method in this.methods) { if (method && typeof this.methods[method] === 'function') { methods.push(method) } } return methods.indexOf(value) !== -1 } /** * 格式化提示信息模板 */ formatTpl(source, params) { const that = this if (arguments.length === 1) { return function () { let args = Array.from(arguments) args.unshift(source) return that.formatTpl.apply(this, args) } } if (params === undefined) { return source } if (arguments.length > 2 && params.constructor !== Array) { params = Array.from(arguments).slice(1) } if (params.constructor !== Array) { params = [params] } params.forEach(function (n, i) { source = source.replace(new RegExp("\\{" + i + "\\}", "g"), function () { return n }) }) return source } /** * 判斷規則依賴是否存在 */ depend(param) { switch (typeof param) { case 'boolean': param = param break case 'string': param = !!param.length break case 'function': param = param() default: param = !0 } return param } /** * 判斷輸入值是否爲空 */ optional(value) { return !this.methods.required(value) && 'dependency-mismatch' } /** * 獲取自定義字段的提示信息 * @param {String} param 字段名 * @param {Object} rule 規則 */ customMessage(param, rule) { const params = this.messages[param] const isObject = typeof params === 'object' if (params && isObject) return params[rule.method] } /** * 獲取某個指定字段的提示信息 * @param {String} param 字段名 * @param {Object} rule 規則 */ defaultMessage(param, rule) { let message = this.customMessage(param, rule) || this.defaults.messages[rule.method] let type = typeof message if (type === 'undefined') { message = `Warning: No message defined for ${rule.method}.` } else if (type === 'function') { message = message.call(this, rule.parameters) } return message } /** * 緩存錯誤信息 * @param {String} param 字段名 * @param {Object} rule 規則 * @param {String} value 元素的值 */ formatTplAndAdd(param, rule, value) { let msg = this.defaultMessage(param, rule) this.errorList.push({ param: param, msg: msg, value: value, }) } /** * 驗證某個指定字段的規則 * @param {String} param 字段名 * @param {Object} rules 規則 * @param {Object} data 須要驗證的數據對象 */ checkParam(param, rules, data) { // 緩存數據對象 this.data = data // 緩存字段對應的值 const value = data[param] !== null && data[param] !== undefined ? data[param] : '' // 遍歷某個指定字段的全部規則,依次驗證規則,不然緩存錯誤信息 for (let method in rules) { // 判斷驗證方法是否存在 if (this.isValidMethod(method)) { // 緩存規則的屬性及值 const rule = { method: method, parameters: rules[method] } // 調用驗證方法 const result = this.methods[method](value, rule.parameters) // 若result返回值爲dependency-mismatch,則說明該字段的值爲空或非必填字段 if (result === 'dependency-mismatch') { continue } this.setValue(param, method, result, value) // 判斷是否經過驗證,不然緩存錯誤信息,跳出循環 if (!result) { this.formatTplAndAdd(param, rule, value) break } } } } /** * 設置字段的默認驗證值 * @param {String} param 字段名 */ setView(param) { this.form[param] = { $name: param, $valid: true, $invalid: false, $error: {}, $success: {}, $viewValue: ``, } } /** * 設置字段的驗證值 * @param {String} param 字段名 * @param {String} method 字段的方法 * @param {Boolean} result 是否經過驗證 * @param {String} value 字段的值 */ setValue(param, method, result, value) { const params = this.form[param] params.$valid = result params.$invalid = !result params.$error[method] = !result params.$success[method] = result params.$viewValue = value } /** * 驗證全部字段的規則,返回驗證是否經過 * @param {Object} data 須要驗證數據對象 */ checkForm(data) { this.__initData() for (let param in this.rules) { this.setView(param) this.checkParam(param, this.rules[param], data) } return this.valid() } /** * 返回驗證是否經過 */ valid() { return this.size() === 0 } /** * 返回錯誤信息的個數 */ size() { return this.errorList.length } /** * 返回全部錯誤信息 */ validationErrors() { return this.errorList } } export default WxValidate