WebApp【公共组件库】@前端(For Git Submodule)
Tevin
2020-12-01 c4c9a03d3ba029b613d64a6514c132d28636d1c5
表单第三部分,地址联动、手机验证码、开关,同意协议等
10 files added
1 files renamed
7 files modified
618 ■■■■■ changed files
common/sassMixin.scss 4 ●●●● patch | view | raw | blame | history
forms/chinaArea/CChinaArea.vue 139 ●●●●● patch | view | raw | blame | history
forms/chinaArea/ChinaLocationData.json patch | view | raw | blame | history
forms/chinaArea/ChinaLocations.js 118 ●●●●● patch | view | raw | blame | history
forms/chinaArea/cChinaArea.scss 23 ●●●●● patch | view | raw | blame | history
forms/chinaArea/index.js 12 ●●●●● patch | view | raw | blame | history
forms/form/CForm.vue 8 ●●●● patch | view | raw | blame | history
forms/form/CFormAgreement.vue 81 ●●●●● patch | view | raw | blame | history
forms/form/CFormSubmit.vue 5 ●●●●● patch | view | raw | blame | history
forms/form/cForm.scss 36 ●●●●● patch | view | raw | blame | history
forms/form/index.js 2 ●●●●● patch | view | raw | blame | history
forms/input/CInputPhoneCode.vue 77 ●●●●● patch | view | raw | blame | history
forms/input/cInputPhoneCode.scss 17 ●●●●● patch | view | raw | blame | history
forms/input/index.js 2 ●●●●● patch | view | raw | blame | history
forms/select/CSelect.vue 11 ●●●● patch | view | raw | blame | history
forms/switch/CSwitch.vue 42 ●●●●● patch | view | raw | blame | history
forms/switch/cSwitch.scss 31 ●●●●● patch | view | raw | blame | history
forms/switch/index.js 10 ●●●●● patch | view | raw | blame | history
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
 */
forms/chinaArea/CChinaArea.vue
New file
@@ -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>
forms/chinaArea/ChinaLocationData.json
forms/chinaArea/ChinaLocations.js
New file
@@ -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;
    },
};
forms/chinaArea/cChinaArea.scss
New file
@@ -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;
        }
    }
}
forms/chinaArea/index.js
New file
@@ -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,
}
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);
        };
forms/form/CFormAgreement.vue
New file
@@ -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>
forms/form/CFormSubmit.vue
@@ -1,3 +1,8 @@
/**
 * CFormSubmit
 * @author Tevin
 */
<template>
    <view class="c-form-submit">
        <button
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;
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,
}
forms/input/CInputPhoneCode.vue
New file
@@ -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>
forms/input/cInputPhoneCode.scss
New file
@@ -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;
        }
    }
}
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,
}
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];
forms/switch/CSwitch.vue
New file
@@ -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>
forms/switch/cSwitch.scss
New file
@@ -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;
        }
    }
}
forms/switch/index.js
New file
@@ -0,0 +1,10 @@
/**
 * CSwitch
 * @author Tevin
 */
import CSwitch from '@components/forms/switch/CSwitch.vue';
export {
    CSwitch,
}