├── .babelrc ├── index.js ├── demo ├── demo.scss ├── index.tpl.html ├── demo.js └── webpack.js ├── Gruntfile.js ├── lib ├── areaGear.scss ├── select.scss ├── areas.scss ├── areas.js ├── areaGear.js └── select.js ├── src ├── areaGear.scss ├── select.scss ├── areas.scss ├── areas.js ├── areaGear.js └── select.js ├── LICENSE ├── package.json └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets":[ 3 | "es2015", 4 | "react" 5 | ], 6 | "plugins":[] 7 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by chkui on 2017/7/7. 3 | */ 4 | module.exports = require('./lib/areas').default 5 | module.exports.default = module.exports 6 | -------------------------------------------------------------------------------- /demo/demo.scss: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | html { 4 | position: relative; 5 | font-size: 20px; /*由于chrome内核的浏览器最小标定字体为12px因此使用1rem=20px来换算布局大小*/ 6 | height: 100%; 7 | * { 8 | box-sizing: border-box; //全局使用盒模型 9 | } 10 | } -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.initConfig({ 3 | copy: { 4 | main: { 5 | files: [ 6 | //根目录文件打包 7 | {expand: true, cwd: 'src/', src: '**', dest: 'lib/'}, 8 | ], 9 | }, 10 | }, 11 | }); 12 | grunt.loadNpmTasks('grunt-contrib-copy'); 13 | grunt.registerTask('default', ['copy']); 14 | }; -------------------------------------------------------------------------------- /demo/index.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 测试DEMO 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /lib/areaGear.scss: -------------------------------------------------------------------------------- 1 | .area_roll_mask { 2 | //-webkit-mask: -webkit-gradient(linear, 0% 40%, 0% 0%, from(#debb47), to(rgba(36, 142, 36, 0))); 3 | //-webkit-mask: -webkit-linear-gradient(bottom, #debb47 50%, rgba(36, 142, 36, 0)); 4 | padding: 0 5 | } 6 | 7 | .area_roll { 8 | display: flex; 9 | width: 100%; 10 | height: auto; 11 | overflow: hidden; 12 | background-color: transparent; 13 | //-webkit-mask: -webkit-gradient(linear, 0% 50%, 0% 100%, from(#debb47), to(rgba(36, 142, 36, 0))); 14 | //-webkit-mask: -webkit-linear-gradient(top, #debb47 50%, rgba(36, 142, 36, 0)) 15 | } 16 | 17 | .area_roll > div:nth-child(3) .area_grid > div { 18 | left: 42% 19 | } -------------------------------------------------------------------------------- /src/areaGear.scss: -------------------------------------------------------------------------------- 1 | .area_roll_mask { 2 | //-webkit-mask: -webkit-gradient(linear, 0% 40%, 0% 0%, from(#debb47), to(rgba(36, 142, 36, 0))); 3 | //-webkit-mask: -webkit-linear-gradient(bottom, #debb47 50%, rgba(36, 142, 36, 0)); 4 | padding: 0 5 | } 6 | 7 | .area_roll { 8 | display: flex; 9 | width: 100%; 10 | height: auto; 11 | overflow: hidden; 12 | background-color: transparent; 13 | //-webkit-mask: -webkit-gradient(linear, 0% 50%, 0% 100%, from(#debb47), to(rgba(36, 142, 36, 0))); 14 | //-webkit-mask: -webkit-linear-gradient(top, #debb47 50%, rgba(36, 142, 36, 0)) 15 | } 16 | 17 | .area_roll > div:nth-child(3) .area_grid > div { 18 | left: 42% 19 | } -------------------------------------------------------------------------------- /lib/select.scss: -------------------------------------------------------------------------------- 1 | $baseHeight:2rem; 2 | .gear-box { 3 | position: relative; 4 | font-size: 16px; 5 | height: $baseHeight*5; 6 | float: left; 7 | background-color: transparent; 8 | overflow: hidden; 9 | flex: 1 10 | } 11 | 12 | .gear { 13 | position: absolute; 14 | width: 100%; 15 | z-index: 9902; 16 | margin-top: $baseHeight*2; 17 | top:0; 18 | } 19 | 20 | .tooth { 21 | height: $baseHeight; 22 | line-height: $baseHeight; 23 | text-align: center; 24 | display: flex; 25 | line-clamp: 1; 26 | flex-direction: column; 27 | overflow: hidden 28 | } 29 | 30 | .area_grid { 31 | position: relative; 32 | top: $baseHeight*2; 33 | width: 100%; 34 | height: $baseHeight; 35 | margin: 0; 36 | box-sizing: border-box; 37 | z-index: 0; 38 | border-top: 1px solid #abaeb5; 39 | border-bottom: 1px solid #abaeb5 40 | } -------------------------------------------------------------------------------- /src/select.scss: -------------------------------------------------------------------------------- 1 | $baseHeight:2rem; 2 | .gear-box { 3 | position: relative; 4 | font-size: 16px; 5 | height: $baseHeight*5; 6 | float: left; 7 | background-color: transparent; 8 | overflow: hidden; 9 | flex: 1 10 | } 11 | 12 | .gear { 13 | position: absolute; 14 | width: 100%; 15 | z-index: 9902; 16 | margin-top: $baseHeight*2; 17 | top:0; 18 | } 19 | 20 | .tooth { 21 | height: $baseHeight; 22 | line-height: $baseHeight; 23 | text-align: center; 24 | display: flex; 25 | line-clamp: 1; 26 | flex-direction: column; 27 | overflow: hidden 28 | } 29 | 30 | .area_grid { 31 | position: relative; 32 | top: $baseHeight*2; 33 | width: 100%; 34 | height: $baseHeight; 35 | margin: 0; 36 | box-sizing: border-box; 37 | z-index: 0; 38 | border-top: 1px solid #abaeb5; 39 | border-bottom: 1px solid #abaeb5 40 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by chkui on 2017/7/7. 3 | */ 4 | 5 | import React from 'react' 6 | import {render} from 'react-dom' 7 | 8 | import RAreas from '../index' 9 | import './demo.scss' 10 | 11 | class Demo extends React.Component{ 12 | constructor(...props){ 13 | super(...props) 14 | this.state = { 15 | show:false, 16 | area:'' 17 | } 18 | this.popHandle = this.popHandle.bind(this) 19 | this.cancelHandle = this.cancelHandle.bind(this) 20 | this.commitHandle = this.commitHandle.bind(this) 21 | } 22 | 23 | popHandle(){ 24 | this.setState({ 25 | show:true 26 | }) 27 | } 28 | 29 | cancelHandle(){ 30 | this.setState({ 31 | show:false 32 | }) 33 | } 34 | 35 | commitHandle(data){ 36 | this.setState({ 37 | show:false, 38 | area:`${data.province.name}${data.city.name}${data.area.name}` 39 | }) 40 | } 41 | 42 | render(){ 43 | return( 44 |
45 | 46 |

选择结果:

47 |

{this.state.area}

48 | {this.state.show && ()} 49 |
50 | ) 51 | } 52 | } 53 | 54 | render(, document.getElementById('root')) 55 | 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rareas", 3 | "version": "1.0.9", 4 | "description": "area select input for mobile", 5 | "main": "index.js", 6 | "scripts": { 7 | "demo": "webpack-dev-server --progress --colors --inline --config ./demo/webpack", 8 | "babel": "grunt && babel ./src/ -d ./lib/", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/palmg/RAreaS.git" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "select input", 18 | "area select" 19 | ], 20 | "author": "chkui", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/palmg/RAreaS/issues" 24 | }, 25 | "homepage": "https://github.com/palmg/RAreaS#readme", 26 | "dependencies": { 27 | "autoprefixer": "^7.1.1", 28 | "babel-core": "^6.24.1", 29 | "babel-loader": "^6.2.7", 30 | "babel-preset-es2015": "^6.18.0", 31 | "babel-preset-react": "^6.16.0", 32 | "classnames": "^2.2.5", 33 | "css-loader": "^0.26.0", 34 | "node-sass": "^4.1.1", 35 | "postcss-loader": "^1.2.1", 36 | "precss": "^1.4.0", 37 | "sass-loader": "^4.0.1", 38 | "style-loader": "^0.13.1" 39 | }, 40 | "devDependencies": { 41 | "grunt": "^1.0.1", 42 | "grunt-contrib-copy": "^1.0.0", 43 | "file-loader": "^0.10.1", 44 | "html-loader": "^0.4.4", 45 | "html-webpack-plugin": "^2.24.1", 46 | "react": "^16.1.1", 47 | "react-dom": "^16.1.1", 48 | "url-loader": "^0.5.7", 49 | "webpack": "^2.6.1", 50 | "webpack-dev-server": "^2.4.5" 51 | } 52 | } -------------------------------------------------------------------------------- /demo/webpack.js: -------------------------------------------------------------------------------- 1 | const path = require('path'), 2 | webpack = require('webpack'), 3 | HtmlWebpackPlugin = require('html-webpack-plugin') 4 | 5 | module.exports = { 6 | devtool: 'source-map', 7 | context: path.resolve(__dirname), 8 | entry: { 9 | bundle: './demo.js', 10 | vendor: ['react','react-dom'] 11 | }, 12 | output: { 13 | path: path.resolve(__dirname, "./dist"), 14 | filename: '[name].js', 15 | publicPath: '/' 16 | }, 17 | module: { 18 | rules: [{ 19 | test: /\.js$/, 20 | use: [{ 21 | loader: 'babel-loader', 22 | options: { presets: ['es2015', 'react'] } 23 | }], 24 | exclude: /node_modules/ 25 | }, { 26 | test: /\.scss$/, 27 | use: [ 28 | 'style-loader', 29 | 'css-loader?modules&camelCase&importLoaders=1&localIdentName=[local][hash:base64:8]', 30 | { 31 | loader:'postcss-loader', 32 | options: { 33 | plugins: function() { 34 | return [ 35 | require('autoprefixer')() 36 | ]; 37 | } 38 | } 39 | }, 40 | 'sass-loader' 41 | ] 42 | }, { 43 | test: /\.html$/, 44 | use: ['html-loader?minimize=false'] 45 | }] 46 | }, 47 | plugins: [ 48 | new HtmlWebpackPlugin({ 49 | filename: path.resolve(__dirname, './dist/index.html'), 50 | template: path.resolve(__dirname, './index.tpl.html'), 51 | }), 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /lib/areas.scss: -------------------------------------------------------------------------------- 1 | .gearArea { 2 | display: block; 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | z-index: 9900; 9 | overflow: hidden; 10 | animation-fill-mode: both; 11 | background-color: rgba(0, 0, 0, 0.2); 12 | box-sizing: border-box; 13 | font-size: .7rem; 14 | } 15 | 16 | .area_ctrl { 17 | vertical-align: middle; 18 | background-color: #d5d8df; 19 | color: #000; 20 | margin: 0; 21 | height: auto; 22 | width: 100%; 23 | position: fixed; 24 | left: 0; 25 | bottom: 0; 26 | z-index: 9901; 27 | overflow: hidden; 28 | transform: translate3d(0, 0, 0) 29 | } 30 | 31 | .slideInUp { 32 | animation: slideInUp .3s; 33 | } 34 | 35 | @keyframes slideInUp { 36 | from { 37 | transform: translate3d(0, 100%, 0) 38 | } 39 | to { 40 | transform: translate3d(0, 0, 0) 41 | } 42 | } 43 | 44 | .area_btn { 45 | color: #0575f2; 46 | font-size: .8rem; 47 | line-height: 1; 48 | text-align: center; 49 | padding: .8rem 1rem; 50 | } 51 | 52 | .area_btn_box:before, 53 | .area_btn_box:after { 54 | content: ''; 55 | position: absolute; 56 | height: 1px; 57 | width: 100%; 58 | display: block; 59 | background-color: #96979b; 60 | z-index: 15; 61 | transform: scaleY(0.33) 62 | } 63 | 64 | .area_btn_box { 65 | display: flex; 66 | justify-content: space-between; 67 | align-items: stretch; 68 | background-color: #f1f2f4; 69 | position: relative 70 | } 71 | 72 | .area_btn_box:before { 73 | left: 0; 74 | top: 0; 75 | transform-origin: 50% 20% 76 | } 77 | 78 | .area_btn_box:after { 79 | left: 0; 80 | bottom: 0; 81 | transform-origin: 50% 70% 82 | } -------------------------------------------------------------------------------- /src/areas.scss: -------------------------------------------------------------------------------- 1 | .gearArea { 2 | display: block; 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | z-index: 9900; 9 | overflow: hidden; 10 | animation-fill-mode: both; 11 | background-color: rgba(0, 0, 0, 0.2); 12 | box-sizing: border-box; 13 | font-size: .7rem; 14 | } 15 | 16 | .area_ctrl { 17 | vertical-align: middle; 18 | background-color: #d5d8df; 19 | color: #000; 20 | margin: 0; 21 | height: auto; 22 | width: 100%; 23 | position: fixed; 24 | left: 0; 25 | bottom: 0; 26 | z-index: 9901; 27 | overflow: hidden; 28 | transform: translate3d(0, 0, 0) 29 | } 30 | 31 | .slideInUp { 32 | animation: slideInUp .3s; 33 | } 34 | 35 | @keyframes slideInUp { 36 | from { 37 | transform: translate3d(0, 100%, 0) 38 | } 39 | to { 40 | transform: translate3d(0, 0, 0) 41 | } 42 | } 43 | 44 | .area_btn { 45 | color: #0575f2; 46 | font-size: .8rem; 47 | line-height: 1; 48 | text-align: center; 49 | padding: .8rem 1rem; 50 | } 51 | 52 | .area_btn_box:before, 53 | .area_btn_box:after { 54 | content: ''; 55 | position: absolute; 56 | height: 1px; 57 | width: 100%; 58 | display: block; 59 | background-color: #96979b; 60 | z-index: 15; 61 | transform: scaleY(0.33) 62 | } 63 | 64 | .area_btn_box { 65 | display: flex; 66 | justify-content: space-between; 67 | align-items: stretch; 68 | background-color: #f1f2f4; 69 | position: relative 70 | } 71 | 72 | .area_btn_box:before { 73 | left: 0; 74 | top: 0; 75 | transform-origin: 50% 20% 76 | } 77 | 78 | .area_btn_box:after { 79 | left: 0; 80 | bottom: 0; 81 | transform-origin: 50% 70% 82 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React实现的移动端区域选择工具。 2 | ## 安装 3 | `npm install rareas` 4 | ## 使用 5 | 通过React组件的方式使用: 6 | ```JavaScript 7 | import React from 'react' 8 | import {render} from 'react-dom' 9 | import RAreas from 'RAreas' //引入RAreas组件 const RAreas = require('RAreas') 10 | render() //使用 11 | ``` 12 | 组件默认提供中国的省市区选项。也可以自行传入区域定义的区域参数。 13 | ## 组件接口 14 | 接口 | 类型 | 说明 15 | ----- | ---- | --- 16 | list | array | 区域定义列表。列表的结构为:
`[{name:'',id:'',child:[{id:'',name:'',child:[{id:'',name:''}]}]}]`。
目前只支持三级结构。**如果不指定的话默认会使用内置的中国省市区列表。** 17 | onCancel | function | 当取消按钮被点击时触发的回调。`(event)=>{}` 18 | onCommit | function | 当确定按钮被点击时触发的回调。`(data,event)=>{}`。
`data`参数表示当前所选的地域。其结构为:`{province: {id: name:}city: {id: name:}area: {id: name:}}`。对应传入的数据。 19 | ## 打包说明 20 | 组件使用es6和scss开发,所以需要引入babel和node-sass打包。相关包已经通过*package.json*的`dependencies`参数引入。只要对webpack进行以下配置即可。 21 | ```JavaScript 22 | module: { 23 | rules: [{ 24 | test: /\.js(x)$/, 25 | use: [{ 26 | loader: 'babel-loader', 27 | options: { presets: ['es2015', 'react'] } //处理es6和react 28 | }], 29 | exclude: /node_modules/ 30 | }, { 31 | test: /\.scss$/, //处理scss或sass样式。 32 | use: [ 33 | 'style-loader', 34 | 'css-loader', 35 | {//postcss用于为样式自动添加浏览器头(例如:-webkit),如无需要,可以移除 36 | loader:'postcss-loader', 37 | options: { 38 | plugins: function() { 39 | return [ 40 | require('autoprefixer')() 41 | ]; 42 | } 43 | } 44 | }, 45 | 'sass-loader' //sass加载器 46 | ] 47 | } 48 | //your rules 49 | ] 50 | } 51 | ``` 52 | 如果nodejs的版本过低(7.2以下)安装最新的sass会报错,请单独使用cnpm安装node-sass: 53 | ```bash 54 | $ npm rm node-sass 55 | $ cnpm install node-sass 56 | ``` 57 | 58 | **本项目的默认地域数据来自LArea开源项目:https://github.com/xfhxbb/LArea。感谢他们的辛勤工作。** -------------------------------------------------------------------------------- /src/areas.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by chkui on 2017/7/7. 3 | */ 4 | 5 | import React from 'react' 6 | import cnTool from 'classnames/bind' 7 | import Gear from './select' 8 | import styles from './areas.scss' 9 | import areaData from './areaData' 10 | import AreaGear from './areaGear' 11 | const cn = cnTool.bind(styles) 12 | 13 | /** 14 | * 移动端——三级区域选择。 15 | * @param {array} list 传入的区域列表.结构为: 16 | * [{ 17 | * name:'',省 18 | * id:'', 19 | * child:[{ 20 | * id:'', 21 | * name:'',市 22 | * child:[{ 23 | * 区 24 | * }] 25 | * }] 26 | * }] 27 | * @param {function} onCancel (e)=>{} 取消按钮事件 28 | * @param {function} onCommit (data,e)=>{}确定按钮事件,data的结构为: 29 | * { 30 | * province: {id: name:} 31 | * city: {id: name:} 32 | * area: {id: name:} 33 | * } 34 | */ 35 | class Areas extends React.Component { 36 | constructor(...props) { 37 | super(...props) 38 | this.deepClone = this.deepClone.bind(this) 39 | this.submitHandle = this.submitHandle.bind(this) 40 | } 41 | 42 | submitHandle(e) { 43 | const { onCommit } = this.props 44 | if (onCommit) { 45 | const address = this.deepClone(this.gear.data) 46 | delete address.province.child 47 | delete address.city.child 48 | onCommit(address) 49 | } 50 | } 51 | 52 | deepClone(currobj) { 53 | let newobj = {} 54 | for (var i in currobj) newobj[i] = typeof currobj[i] === 'object' ? this.deepClone(currobj[i]) : currobj[i] 55 | return newobj 56 | } 57 | 58 | render() { 59 | return ( 60 |
61 |
62 |
取消
63 |
确定
64 |
65 | this.gear = ref} list={this.props.list || areaData} /> 66 |
67 | ) 68 | } 69 | } 70 | 71 | export default Areas 72 | -------------------------------------------------------------------------------- /src/areaGear.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by chkui on 2017/7/10. 3 | */ 4 | 5 | import React from 'react' 6 | import cnTool from 'classnames/bind' 7 | import Select from './select' 8 | import styles from './areaGear.scss' 9 | const cn = cnTool.bind(styles) 10 | 11 | /** 12 | * 移动端——三级区域选择。 13 | * @param {array} list 传入的区域列表。结构分为省市区三级: 14 | * [{ 15 | * name:'',省 16 | * id:'', 17 | * child:[{ 18 | * id:'', 19 | * name:'',市 20 | * child:[{ 21 | * 区 22 | * }] 23 | * }] 24 | * }] 25 | * 可以通过ref的this.data获取数据,结构为: 26 | * { 27 | * province: {id: name:} 28 | * city: {id: name:} 29 | * area: {id: name:} 30 | * } 31 | */ 32 | class areaGear extends React.Component { 33 | constructor(...props) { 34 | super(...props) 35 | const provinceList = this.props.list, 36 | province = provinceList[0], 37 | cityList = province.child || [{ id: 'noneCity', name: '' }], 38 | city = cityList[0], 39 | areaList = city.child || [{ id: 'noneArea', name: '' }] 40 | this.state = { 41 | province: provinceList, 42 | city: cityList, 43 | area: areaList 44 | } 45 | this.data = { 46 | province: province, 47 | city: city, 48 | area: areaList[0] 49 | } 50 | this.provinceHandle = this.provinceHandle.bind(this) 51 | this.cityHandle = this.cityHandle.bind(this) 52 | this.areaHandle = this.areaHandle.bind(this) 53 | } 54 | 55 | //type=['province'|'city'|'area'] 56 | buildData(self, type) { 57 | let { province, city, area } = this.state 58 | switch (type) { 59 | case 'province': 60 | city = self.child || [{ id: 'noneCity', name: '' }] 61 | area = city[0].child || [{ id: 'noneArea', name: '' }] 62 | this.data = { 63 | province: self, 64 | city: city[0], 65 | area: area[0] 66 | } 67 | break 68 | case 'city': 69 | area = self.child || [{ id: 'noneArea', name: '' }] 70 | this.data.city = self 71 | this.data.area = area[0] 72 | break 73 | case 'area': 74 | this.data.area = self 75 | break 76 | 77 | } 78 | this.setState({ 79 | province: province, 80 | city: city, 81 | area: area 82 | }) 83 | } 84 | 85 | provinceHandle(self) { 86 | this.buildData(self, 'province') 87 | } 88 | 89 | cityHandle(self) { 90 | this.buildData(self, 'city') 91 | } 92 | 93 | areaHandle(self) { 94 | this.buildData(self, 'area') 95 | } 96 | 97 | render() { 98 | const { province, city, area } = this.state 99 | return ( 100 |
101 |
102 | 104 |