表单第三部分,地址联动、手机验证码、开关,同意协议等
10 files added
1 files renamed
7 files modified
| | |
| | | * @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 |
| | | */ |
New file |
| | |
| | | /** |
| | | * 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> |
New file |
| | |
| | | /** |
| | | * 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; |
| | | }, |
| | | }; |
New file |
| | |
| | | /** |
| | | * 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; |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | /** |
| | | * CChinaArea |
| | | * @author Tevin |
| | | */ |
| | | |
| | | import CChinaArea from '@components/forms/chinaArea/CChinaArea.vue'; |
| | | import ChinaLocations from '@components/forms/chinaArea/ChinaLocations'; |
| | | |
| | | export { |
| | | CChinaArea, |
| | | ChinaLocations, |
| | | } |
| | |
| | | Taro.showToast({ |
| | | title: validation.msg, |
| | | icon: 'none', |
| | | mask: true, |
| | | mask: false, |
| | | duration: 2000, |
| | | }); |
| | | return; |
| | |
| | | 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); |
| | | }; |
New file |
| | |
| | | /** |
| | | * 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> |
| | |
| | | /** |
| | | * CFormSubmit |
| | | * @author Tevin |
| | | */ |
| | | |
| | | <template> |
| | | <view class="c-form-submit"> |
| | | <button |
| | |
| | | } |
| | | } |
| | | } |
| | | .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; |
| | |
| | | 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, |
| | | } |
New file |
| | |
| | | /** |
| | | * 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> |
New file |
| | |
| | | /** |
| | | * 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; |
| | | } |
| | | } |
| | | } |
| | |
| | | */ |
| | | |
| | | import CInput from '@components/forms/input/CInput.vue'; |
| | | import CInputPhoneCode from '@components/forms/input/CInputPhoneCode.vue'; |
| | | |
| | | export { |
| | | CInput, |
| | | CInputPhoneCode, |
| | | } |
| | |
| | | <view class="c-select"> |
| | | <picker |
| | | mode="selector" |
| | | :range="range" |
| | | :range="options" |
| | | :value="current" |
| | | range-key="name" |
| | | @change="evt=>handleChange(evt.detail)" |
| | | > |
| | | <AtInput |
| | |
| | | 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]; |
New file |
| | |
| | | /** |
| | | * 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> |
New file |
| | |
| | | /** |
| | | * 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; |
| | | } |
| | | } |
| | | } |
New file |
| | |
| | | /** |
| | | * CSwitch |
| | | * @author Tevin |
| | | */ |
| | | |
| | | import CSwitch from '@components/forms/switch/CSwitch.vue'; |
| | | |
| | | export { |
| | | CSwitch, |
| | | } |