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