From c4c9a03d3ba029b613d64a6514c132d28636d1c5 Mon Sep 17 00:00:00 2001 From: Tevin <tingquanren@163.com> Date: Tue, 01 Dec 2020 14:48:50 +0800 Subject: [PATCH] 表单第三部分,地址联动、手机验证码、开关,同意协议等 --- forms/chinaArea/ChinaLocations.js | 118 ++++++++++ forms/chinaArea/ChinaLocationData.json | 0 forms/switch/index.js | 10 forms/chinaArea/cChinaArea.scss | 23 ++ forms/form/cForm.scss | 36 +++ forms/input/cInputPhoneCode.scss | 17 + forms/input/index.js | 2 forms/form/CFormSubmit.vue | 5 common/sassMixin.scss | 4 forms/switch/cSwitch.scss | 31 ++ forms/form/index.js | 2 forms/switch/CSwitch.vue | 42 +++ forms/input/CInputPhoneCode.vue | 77 +++++++ forms/chinaArea/CChinaArea.vue | 139 ++++++++++++ forms/form/CForm.vue | 8 forms/select/CSelect.vue | 11 forms/chinaArea/index.js | 12 + forms/form/CFormAgreement.vue | 81 +++++++ 18 files changed, 608 insertions(+), 10 deletions(-) diff --git a/common/sassMixin.scss b/common/sassMixin.scss index 04a2f68..888c160 100644 --- a/common/sassMixin.scss +++ b/common/sassMixin.scss @@ -104,11 +104,11 @@ * @include flexbox(); * $mode: * flex / inline - * $align: + * $align: <justify-content> <align-items> <align-content> * flex-start / flex-end / center / space-between / space-around * flex-start / flex-end / center / baseline / stretch * flex-start / flex-end / center / space-between / space-around / stretch - * $flow: + * $flow: <flex-direction> <flex-wrap> * row / row-reverse / column / column-reverse * nowrap / wrap / wrap-reverse */ diff --git a/forms/chinaArea/CChinaArea.vue b/forms/chinaArea/CChinaArea.vue new file mode 100644 index 0000000..5bfe017 --- /dev/null +++ b/forms/chinaArea/CChinaArea.vue @@ -0,0 +1,139 @@ +/** + * CChinaArea + * @author Tevin + */ + +<template> + <view class="c-china-area"> + <picker + mode="multiSelector" + :range="range" + :value="current" + range-key="label" + @columnchange="evt=>updateColumns(evt.detail)" + @change="evt=>handleChange(evt.detail.value)" + > + <view @tap="evt=>handleOpen(evt)"> + <AtInput + ref="input" + :name="itemData.name" + :title="itemData.label" + :required="itemData.required" + :error="itemData.error" + :placeholder="placeholder" + :value="selected" + > + <view class="at-icon at-icon-chevron-right" /> + </AtInput> + </view> + </picker> + </view> +</template> + +<script> +import { AtInput } from 'taro-ui-vue'; +import ChinaLocations from './ChinaLocations'; +import './cChinaArea.scss'; + +const { locationTree, getRegionNames } = ChinaLocations; + +export default { + name: 'CChinaArea', + components: { + AtInput, + }, + props: { + placeholder: String, + itemData: Object, + }, + data() { + return { + range: [[], [], []], + current: [0, 0, 0], + }; + }, + computed: { + selected() { + const curVal = this.itemData.formData[this.itemData.name]; + if (curVal && curVal.length === 3) { + return getRegionNames(curVal).join(' / '); + } else { + return ''; + } + }, + }, + methods: { + handleOpen(evt) { + if (evt.target.className.indexOf('at-input__title') >= 0) { + evt.stopPropagation(); + evt.preventDefault(); + return; + } + const curVal = this.itemData.formData[this.itemData.name]; + const range = [ + locationTree, + locationTree[0].children, + locationTree[0].children[0].children, + ]; + const current = [0, 0, 0]; + // 有值 + if (curVal && curVal.length > 0) { + // 省 + if (curVal[0]) { + const proviceIndex = locationTree.findIndex( + (provice) => provice.code === curVal[0] + ); + if (proviceIndex >= 0) { + range[1] = locationTree[proviceIndex].children; + range[2] = locationTree[proviceIndex].children[0].children; + current[0] = proviceIndex; + // 市 + if (curVal[1]) { + const cityIndex = range[1].findIndex((city) => city.code === curVal[1]); + if (cityIndex >= 0) { + range[2] = range[1].children[cityIndex].children; + current[1] = cityIndex; + } + // 区 + if (curVal[2]) { + const areaIndex = range[2].findIndex( + (area) => area.code === curVal[2] + ); + if (areaIndex >= 0) { + current[2] === areaIndex; + } + } + } + } + } + } + this.range = range; + this.current = current; + }, + updateColumns(roll) { + if (roll.column === 0) { + const cities = locationTree[roll.value].children; + this.range.splice(1, 2, cities, cities[0].children); + this.current = [roll.value, 0, 0]; + } else if (roll.column === 1) { + const areas = locationTree[this.current[0]].children[roll.value].children; + this.range.splice(2, 1, areas); + this.current = [this.current[0], roll.value, 0]; + } else if (roll.column === 3) { + this.current.splice(2, 1, roll.value); + } + }, + handleChange(detail) { + const codes = []; + const provice = locationTree[detail[0]]; + codes[0] = provice.value; + const city = provice.children[detail[1]]; + codes[1] = city.value; + const area = city.children[detail[2]]; + codes[2] = area.value; + this.itemData.onChange(codes); + }, + }, + mounted() {}, +}; +</script> \ No newline at end of file diff --git a/common/ChinaLocations.json b/forms/chinaArea/ChinaLocationData.json similarity index 100% rename from common/ChinaLocations.json rename to forms/chinaArea/ChinaLocationData.json diff --git a/forms/chinaArea/ChinaLocations.js b/forms/chinaArea/ChinaLocations.js new file mode 100644 index 0000000..3710d30 --- /dev/null +++ b/forms/chinaArea/ChinaLocations.js @@ -0,0 +1,118 @@ +/** + * ChinaLocations + * @author Tevin + */ + +import ChinaLocationData from './ChinaLocationData.json'; + +const locationTree = []; +Object.keys(ChinaLocationData).forEach((code1) => { + const province = { + label: ChinaLocationData[code1].name, + value: code1, + children: [], + }; + const children1 = ChinaLocationData[code1].children; + Object.keys(children1).forEach((code2) => { + const city = { + label: children1[code2].name, + value: code2, + children: [], + }; + if (typeof children1[code2].children !== 'undefined') { + const children2 = children1[code2].children; + Object.keys(children2).forEach((code3) => { + city.children.push({ + label: children2[code3], + value: code3, + }); + }); + } + province.children.push(city); + }); + locationTree.push(province); +}); + +export default { + locationTree, + // 获取省市区拼合文本 + getRegionText(regions) { + if (regions.length === 0) { + return ''; + } + let address = ''; + let tempLocationData = ChinaLocationData; + regions.forEach((code) => { + if (typeof tempLocationData[code].name === 'string') { + address += tempLocationData[code].name; + } else { + address += tempLocationData[code]; + } + tempLocationData = tempLocationData[code].children; + }); + return address; + }, + // 省市区名称 + getRegionNames(regions) { + if (typeof regions === 'string') { + regions = regions.split(','); + } + if (!regions || regions.length === 0 || !regions[0]) { + return []; + } + let address = []; + let tempLocationData = ChinaLocationData; + regions.forEach((code) => { + if (!tempLocationData[code]) { + return; + } + if (typeof tempLocationData[code].name === 'string') { + address.push(tempLocationData[code].name); + } else { + address.push(tempLocationData[code]); + } + tempLocationData = tempLocationData[code].children; + }); + return address; + }, + // 省市区文本转code + getRegionCodes(regions) { + if (typeof regions === 'string') { + regions = regions.split(','); + } + if (!regions || regions.length === 0 || !regions[0]) { + return ''; + } + const codes = []; + // 省 + for (let provinceCode in ChinaLocationData) { + if (ChinaLocationData.hasOwnProperty(provinceCode)) { + if (ChinaLocationData[provinceCode].name === regions[0]) { + codes[0] = provinceCode; + // 市 + const provinceChildren = ChinaLocationData[provinceCode].children; + for (let cityCode in provinceChildren) { + if (provinceChildren.hasOwnProperty(cityCode)) { + if (provinceChildren[cityCode].name === regions[1]) { + codes[1] = cityCode; + // 区 + const areaChildren = provinceChildren[cityCode].children; + for (let areaCode in areaChildren) { + if (areaChildren.hasOwnProperty(areaCode)) { + if (areaChildren[areaCode] === regions[2]) { + codes[2] = areaCode; + break; + } + } + } + break; + } + } + } + break; + } + } + } + return codes; + }, +}; \ No newline at end of file diff --git a/forms/chinaArea/cChinaArea.scss b/forms/chinaArea/cChinaArea.scss new file mode 100644 index 0000000..bcdf53b --- /dev/null +++ b/forms/chinaArea/cChinaArea.scss @@ -0,0 +1,23 @@ +/** + * CChinaArea + * @author Tevin + */ + +@import "../../common/sassMixin"; + +.c-china-area { + .at-input__input { + .weui-input { + pointer-events: none; + } + } + .at-input__children { + &::after { + display: none; + } + .at-icon-chevron-right { + font-size: 1.024rem; + color: #ccc; + } + } +} \ No newline at end of file diff --git a/forms/chinaArea/index.js b/forms/chinaArea/index.js new file mode 100644 index 0000000..d673bb1 --- /dev/null +++ b/forms/chinaArea/index.js @@ -0,0 +1,12 @@ +/** + * CChinaArea + * @author Tevin + */ + +import CChinaArea from '@components/forms/chinaArea/CChinaArea.vue'; +import ChinaLocations from '@components/forms/chinaArea/ChinaLocations'; + +export { + CChinaArea, + ChinaLocations, +} \ No newline at end of file diff --git a/forms/form/CForm.vue b/forms/form/CForm.vue index 365fcac..a13df65 100644 --- a/forms/form/CForm.vue +++ b/forms/form/CForm.vue @@ -40,7 +40,7 @@ Taro.showToast({ title: validation.msg, icon: 'none', - mask: true, + mask: false, duration: 2000, }); return; @@ -54,7 +54,11 @@ mounted() { this.formData.$handleChange = (evt = []) => { Object.keys(evt).forEach((key) => { - this.formData[key] = evt[key]; + if (typeof this.formData[key] === 'undefined') { + this.$set(this.formData, key, evt[key]); + } else { + this.formData[key] = evt[key]; + } }); this.onChange && this.onChange(evt); }; diff --git a/forms/form/CFormAgreement.vue b/forms/form/CFormAgreement.vue new file mode 100644 index 0000000..2fe2408 --- /dev/null +++ b/forms/form/CFormAgreement.vue @@ -0,0 +1,81 @@ +/** + * CFormAgreement + * @author Tevin + */ + +<template> + <view class="c-form-agreement"> + <view + class="title" + @tap="evt=>handleCheck()" + > + + <view :class="['icon', checked?'checked':'']"> + <text class="at-icon at-icon-check"></text> + </view> + <text class="tips">我已阅读并同意</text> + </view> + <view + class="protocol" + @tap="evt=>openProtocol()" + >{{protocol}}</view> + <AtFloatLayout + :title="protocol" + :isOpened="protocolShow" + :onClose="evt=>closeProtocol()" + > + <slot /> + </AtFloatLayout> + </view> +</template> + +<script> +import { AtFloatLayout } from 'taro-ui-vue'; + +export default { + name: 'CFormAgreement', + components: { + AtFloatLayout, + }, + props: { + formData: Object, + protocol: String, + }, + data() { + return { + name: 'agreement', + protocolShow: false, + checked: false, + }; + }, + methods: { + handleCheck() { + this.checked = !this.checked; + }, + openProtocol() { + this.protocolShow = true; + }, + closeProtocol() { + this.protocolShow = false; + }, + }, + mounted() { + this.$nextTick(() => { + this.formData.$regItemValidator(this.name, () => { + if (this.checked) { + return Promise.resolve({ + name: this.name, + passed: true, + }); + } else { + return { + name: this.name, + passed: false, + msg: '请同意' + this.protocol, + }; + } + }); + }); + }, +}; +</script> \ No newline at end of file diff --git a/forms/form/CFormSubmit.vue b/forms/form/CFormSubmit.vue index 87b7e2d..3d6d0ff 100644 --- a/forms/form/CFormSubmit.vue +++ b/forms/form/CFormSubmit.vue @@ -1,3 +1,8 @@ +/** + * CFormSubmit + * @author Tevin + */ + <template> <view class="c-form-submit"> <button diff --git a/forms/form/cForm.scss b/forms/form/cForm.scss index 07140a1..976150c 100644 --- a/forms/form/cForm.scss +++ b/forms/form/cForm.scss @@ -16,6 +16,42 @@ } } } + .c-form-agreement { + padding: 0.512rem 0.68267rem; + .title { + display: inline; + .icon { + @include flexbox(inline, center center); + margin-top: 0.08533rem; + margin-right: 0.3rem; + width: 0.85333rem; + min-width: 0.85333rem; + height: 0.85333rem; + color: transparent; + font-size: 0.512rem; + line-height: 1; + border: 0.04267rem solid rgba(97, 144, 232, .2); + border-radius: 50%; + background-color: #fff; + box-sizing: border-box; + transition: all .2s; + &.checked { + color: #fff; + border: none; + background-color: #6190e8; + } + } + .tips { + color: rgb(117, 117, 117); + ; + } + } + .protocol { + display: inline; + color: #2b69de; + font-size: 0.65rem; + } + } .c-form-submit { margin-top: 0.8rem; padding: 0 0.42667rem; diff --git a/forms/form/index.js b/forms/form/index.js index 73b1531..c162fa0 100644 --- a/forms/form/index.js +++ b/forms/form/index.js @@ -6,9 +6,11 @@ import CForm from '@components/forms/form/CForm.vue'; import CFormItem from '@components/forms/form/CFormItem.vue'; import CFormSubmit from '@components/forms/form/CFormSubmit.vue'; +import CFormAgreement from '@components/forms/form/CFormAgreement.vue'; export { CForm, CFormItem, CFormSubmit, + CFormAgreement, } \ No newline at end of file diff --git a/forms/input/CInputPhoneCode.vue b/forms/input/CInputPhoneCode.vue new file mode 100644 index 0000000..161473e --- /dev/null +++ b/forms/input/CInputPhoneCode.vue @@ -0,0 +1,77 @@ +/** + * CInputPhoneCode + * @author Tevin + */ + +<template> + <view class="c-input-phone-code"> + <AtInput + :name="itemData.name" + :title="itemData.label" + type="text" + :placeholder="placeholder" + :required="itemData.required" + :error="itemData.error" + :value="itemData.formData[itemData.name]" + :onChange="evt=>itemData.onChange(evt)" + > + <AtButton + class="c-input-phone-btn" + type="primary" + size="small" + :disabled="holding" + :onClick="evt=>handleClick()" + >{{btnText}}</AtButton> + </AtInput> + </view> +</template> + +<script> +import { AtInput, AtButton } from 'taro-ui-vue'; +import './cInputPhoneCode.scss'; + +export default { + name: 'CInputPhoneCode', + components: { + AtInput, + AtButton, + }, + props: { + placeholder: String, + itemData: Object, + onCallPhoneCode: Function, + }, + data() { + return { + cd: 60, + holding: false, + }; + }, + computed: { + btnText() { + if (this.holding) { + return this.cd + '秒后重新获取'; + } else { + return '重新获取验证码'; + } + }, + }, + methods: { + handleClick() { + if (this.holding) { + return; + } + this.holding = true; + this.cd = 60; + this.onCallPhoneCode(); + const timer = setInterval(() => { + this.cd -= 1; + if (this.cd === 0) { + this.holding = false; + this.cd = 60; + } + }, 1000); + }, + }, +}; +</script> \ No newline at end of file diff --git a/forms/input/cInputPhoneCode.scss b/forms/input/cInputPhoneCode.scss new file mode 100644 index 0000000..215e427 --- /dev/null +++ b/forms/input/cInputPhoneCode.scss @@ -0,0 +1,17 @@ +/** + * input-phone-code + * @author Tevin + */ + +@import "../../common/sassMixin"; + +.c-input-phone-code { + .c-input-phone-btn { + margin-right: 0.426rem; + border: none; + background: #1e8ad2; + .at-button__text { + vertical-align: middle; + } + } +} \ No newline at end of file diff --git a/forms/input/index.js b/forms/input/index.js index 76e3937..109a433 100644 --- a/forms/input/index.js +++ b/forms/input/index.js @@ -4,7 +4,9 @@ */ import CInput from '@components/forms/input/CInput.vue'; +import CInputPhoneCode from '@components/forms/input/CInputPhoneCode.vue'; export { CInput, + CInputPhoneCode, } \ No newline at end of file diff --git a/forms/select/CSelect.vue b/forms/select/CSelect.vue index 9d3f847..c7f0381 100644 --- a/forms/select/CSelect.vue +++ b/forms/select/CSelect.vue @@ -7,8 +7,9 @@ <view class="c-select"> <picker mode="selector" - :range="range" + :range="options" :value="current" + range-key="name" @change="evt=>handleChange(evt.detail)" > <AtInput @@ -41,13 +42,11 @@ itemData: Object, }, data() { - return { - optionKey: typeof this.options[0].value === 'undefined' ? 'id' : 'value', - }; + return {}; }, computed: { - range() { - return (this.options || []).map((item) => item.name || item[this.optionKey]); + optionKey() { + return typeof (this.options[0] || {}).value === 'undefined' ? 'id' : 'value'; }, current() { const curVal = this.itemData.formData[this.itemData.name]; diff --git a/forms/switch/CSwitch.vue b/forms/switch/CSwitch.vue new file mode 100644 index 0000000..6171b54 --- /dev/null +++ b/forms/switch/CSwitch.vue @@ -0,0 +1,42 @@ +/** + * CSwitch + * @author Tevin + */ + +<template> + <view :class="['c-switch', className]"> + <AtSwitch + :title="itemData.label" + :checked="itemData.formData[itemData.name]" + :onChange="evt=>itemData.onChange(evt)" + /> + </view> +</template> + +<script> +import { AtSwitch } from 'taro-ui-vue'; +import './cSwitch.scss'; + +export default { + name: 'CSwitch', + components: { AtSwitch }, + props: { + itemData: Object, + }, + data() { + return {}; + }, + computed: { + className() { + let className = ''; + if (this.itemData.required) { + className += 'c-switch-required '; + } + if (this.itemData.error) { + className += 'c-switch-error '; + } + return className; + }, + }, +}; +</script> \ No newline at end of file diff --git a/forms/switch/cSwitch.scss b/forms/switch/cSwitch.scss new file mode 100644 index 0000000..1738612 --- /dev/null +++ b/forms/switch/cSwitch.scss @@ -0,0 +1,31 @@ +/** + * switch + * @author Tevin + */ + +@import "../../common/sassMixin"; + +.c-switch { + &.c-switch-required { + .at-switch__title { + &::before { + display: block; + position: absolute; + top: 50%; + left: -0.5rem; + margin-right: 0.17067rem; + color: #FF4949; + font-size: 0.59733rem; + font-family: SimSun, sans-serif; + line-height: 1; + content: "*"; + transform: translateY(-50%); + } + } + } + &.c-switch-error { + .at-switch__title { + color: #FF4949; + } + } +} \ No newline at end of file diff --git a/forms/switch/index.js b/forms/switch/index.js new file mode 100644 index 0000000..82d6434 --- /dev/null +++ b/forms/switch/index.js @@ -0,0 +1,10 @@ +/** + * CSwitch + * @author Tevin + */ + +import CSwitch from '@components/forms/switch/CSwitch.vue'; + +export { + CSwitch, +} \ No newline at end of file -- Gitblit v1.9.1