├── .babelrc ├── .editorconfig ├── .gitignore ├── README.md ├── index.html ├── package.json ├── src ├── App.vue ├── assets │ └── logo.png ├── h5.vue ├── index.scss └── main.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-3" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | yarn-error.log 6 | 7 | # Editor directories and files 8 | .idea 9 | *.suo 10 | *.ntvs* 11 | *.njsproj 12 | *.sln 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-c-city 2 | 3 | ## 前言 4 | 前面用vue开发了三四个组件了,都是H5的,现在来看看PC是如何玩转组件的?其实和H5相同,样式不同而已。 5 | 6 | ![VUE开发一个组件——Vue PC城市选择](http://cdn.javanx.cn/wp-content/themes/lensnews2.2/images/post/20181127151310.gif) 7 | 8 | #### 相关推荐 9 | [《VUE开发一个组件——日历选择控件》](http://www.javanx.cn/20181105/vue-c-calendar/) 10 | [《VUE开发一个组件——移动端弹出层(IOS版)》](http://www.javanx.cn/20181106/vue-h5-popup/) 11 | [《VUE开发一个组件——Vue tree树形结构》](http://www.javanx.cn/20181123/vue-tree/) 12 | 13 | > 都提供源码,可以去github上面获取。 14 | 15 | ## 城市控件 16 | 开始今天的课题,制作一个PC版的城市选择控件。 17 | 18 | ### 样式制作 19 | 20 | ```html 21 | 32 | ``` 33 | 34 | 通过`focus`获取焦点事件,控制组件的显示,`blur`失去焦点事件,控制组件的隐藏 35 | ```javascript 36 | export default { 37 | name: 'app', 38 | data () { 39 | return { 40 | title: 'web秀 - VUE开发一个组件——Vue PC城市选择', 41 | showCity: false 42 | } 43 | }, 44 | methods: { 45 | hideCityDialog(){ 46 | this.showCity = false; 47 | }, 48 | showCityDialog(){ 49 | this.showCity = true; 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | css布局,外层用相对位置,里层用绝对位置,让城市组件`.city-components`跟着`input`的位置,这里用`box-shadow`来凸显组件。 56 | ```scss 57 | .city{ 58 | position:relative; 59 | .city-components{ 60 | position: absolute; 61 | width: 400px; 62 | height: 200px; 63 | box-shadow: 0 0 4px 0 rgba(117,117,117,0.5); 64 | border-radius: 2px; 65 | padding: 20px 21px; 66 | } 67 | } 68 | ``` 69 | 70 | 初步效果 71 | ![VUE开发一个组件——Vue PC城市选择](http://cdn.javanx.cn/wp-content/themes/lensnews2.2/images/post/20181127151311.gif) 72 | 73 | ### 数据部分 74 | ```json 75 | [ 76 | { 77 | "airportCode": "PEK", 78 | "cityInfo": "BJ-北京-PEK", 79 | "cityName": "北京", 80 | "airportName": "首都", 81 | "status": 1, 82 | "lat": 40.0881944004, 83 | "lng": 116.6033998315 84 | }, 85 | { 86 | "airportCode": "YIE", 87 | "cityInfo": "AES-阿尔山-YIE", 88 | "cityName": "阿尔山市", 89 | "airportName": "伊尔施", 90 | "status": 0, 91 | "lat": 47.3155940318, 92 | "lng": 119.9293992017 93 | }, 94 | { 95 | "airportCode": "AKU", 96 | "cityInfo": "AKS-阿克苏-AKU", 97 | "cityName": "阿克苏地区", 98 | "airportName": "阿克苏", 99 | "status": 0, 100 | "lat": 41.2657516858, 101 | "lng": 80.3049157658 102 | }, 103 | { 104 | "airportCode": "NGQ", 105 | "cityInfo": "AL-阿里-NGQ", 106 | "cityName": "阿里地区", 107 | "airportName": "昆莎", 108 | "status": 0, 109 | "lat": 32.1081287447, 110 | "lng": 80.0637591367 111 | }, 112 | { 113 | "airportCode": "ALA", 114 | "cityInfo": "ALMT-阿尔玛塔-ALA", 115 | "cityName": "阿拉木图", 116 | "airportName": "阿尔玛塔", 117 | "status": 0 118 | }, 119 | { 120 | "airportCode": "RHT", 121 | "cityInfo": "ALSYQ-阿拉善右旗-RHT", 122 | "cityName": "阿拉善右旗", 123 | "airportName": "阿拉善右旗", 124 | "status": 0, 125 | "lat": 39.2338594871, 126 | "lng": 101.449757309 127 | }, 128 | { 129 | "airportCode": "YIW", 130 | "cityInfo": "YW-义乌-YIW", 131 | "cityName": "义乌市", 132 | "airportName": "义乌", 133 | "status": 0, 134 | "lat": 29.3464578386, 135 | "lng": 120.0389750211 136 | }, 137 | { 138 | "airportCode": "ZQZ", 139 | "cityInfo": "ZJK-张家口-ZQZ", 140 | "cityName": "张家口", 141 | "airportName": "张家口", 142 | "status": 0, 143 | "lat": 40.7461174707, 144 | "lng": 114.9436254875 145 | }, 146 | { 147 | "airportCode": "HSN", 148 | "cityInfo": "ZS-舟山-HSN", 149 | "cityName": "舟山", 150 | "airportName": "普陀山", 151 | "status": 0, 152 | "lat": 29.9396135515, 153 | "lng": 122.3683649114 154 | }, 155 | { 156 | "airportCode": "CGO", 157 | "cityInfo": "ZZ-郑州-CGO", 158 | "cityName": "郑州", 159 | "airportName": "新郑", 160 | "status": 1, 161 | "lat": 34.5308189222, 162 | "lng": 113.8526878594 163 | } 164 | ... 165 | ] 166 | ``` 167 | 168 | 这里只有部分数据,主要是给大家看看结构,数组里面包含对象,对象包含多个字段,下面我们将用`airportCode`字段的首字母来分组,排序等。 169 | 170 | 相关推荐[《js数据如何分组排序?》](http://www.javanx.cn/20180823/array-group/) 171 | 172 | #### 分组 173 | 这里的this.dataList就是数据源 174 | ```javascript 175 | let map = {}; // 处理过后的数据对象 176 | let temps = []; // 临时变量 177 | this.dataList.map(item=>{ 178 | if(item.airportCode){ 179 | let ekey = item.airportCode.charAt(0).toUpperCase(); // 根据key值的第一个字母分组,并且转换成大写 180 | temps = map[ekey] || []; // 如果map里面有这个key了,就取,没有就是空数组 181 | temps.push({ 182 | airportCode: item.airportCode, 183 | airportName: item.cityName 184 | }); 185 | map[ekey] = temps; 186 | } 187 | }) 188 | console.log(map); 189 | ``` 190 | 191 | 打印map值 192 | 193 | ![VUE开发一个组件——Vue PC城市选择](http://cdn.javanx.cn/wp-content/themes/lensnews2.2/images/post/20181127154749.png) 194 | 195 | 可以看到已经分组成功,但是这样的数据结构在页面遍历不好处理,我们进一步处理数据 196 | 197 | #### 格式化 198 | 这里的map就是上面得出的结果 199 | ```javascript 200 | let list = []; 201 | for(let gkey in map) { 202 | list.push({ 203 | ckey: gkey, 204 | cityList: map[gkey] 205 | }) 206 | } 207 | 208 | list = list.sort((li1, li2)=> li1.ckey.charCodeAt(0) - li2.ckey.charCodeAt(0)); 209 | console.log(list); 210 | ``` 211 | 212 | ![VUE开发一个组件——Vue PC城市选择](http://cdn.javanx.cn/wp-content/themes/lensnews2.2/images/post/20181127155039.png) 213 | 214 | 处理后的数据是不是看起来更容易理解了?数组包含23的对象,A-Z(中间个别没有),对象两个字段,一个是首字母key,另外一个对象cityList是数组,包含A(Z)的所有机场城市。 215 | 216 | 这时候的结果是不是我们想要的了?请看第一张图,好像是每4个字母一组,同时我们把分组的key也用一个数组存起来,这时候还得重新分组。 217 | ```javascript 218 | let chunk = 4; 219 | let result =[]; 220 | for (var i = 0, j = list.length; i < j; i += chunk) { 221 | result.push(list.slice(i, i + chunk)); 222 | } 223 | console.log(result); 224 | 225 | let cityListKey = []; 226 | result.map(item=>{ 227 | let ckeys = ''; 228 | item.map(ritem=>{ 229 | ckeys += ritem.ckey; 230 | }) 231 | cityListKey.push(ckeys); 232 | }) 233 | console.log(cityListKey); 234 | ``` 235 | 236 | ![VUE开发一个组件——Vue PC城市选择](http://cdn.javanx.cn/wp-content/themes/lensnews2.2/images/post/20181127155716.png) 237 | 238 | 终于数据是我们要的了,这时候直接将数据渲染到页面即可。(当然如果后台能直接给你这样的数据结构,你就感觉感谢吧) 239 | 240 | ### 数据渲染 241 | ```html 242 |
243 | 246 |
247 | ``` 248 | 先把`cityListKey`渲染出来,并添加样式 249 | ```scss 250 | .clearfix{ 251 | &:after{ 252 | content: ''; 253 | display: block; 254 | clear: both; 255 | } 256 | } 257 | li{ 258 | list-style: none; 259 | } 260 | ul{ 261 | padding: 0; 262 | margin: 0; 263 | } 264 | .filter-tabar{ 265 | border-bottom: 1px solid #d7d7d7; 266 | cursor: pointer; 267 | li{ 268 | text-align: center; 269 | padding: 0 14px; 270 | float: left; 271 | padding-bottom: 14px; 272 | font-size: 14px; 273 | margin: 0 8px; 274 | margin-bottom: -1px; 275 | position: relative; 276 | &.active{ 277 | border-bottom: 1px solid #ff7362; 278 | } 279 | } 280 | } 281 | ``` 282 | ![VUE开发一个组件——Vue PC城市选择](http://cdn.javanx.cn/wp-content/themes/lensnews2.2/images/post/20181127160858.png) 283 | 284 | Ok,继续把下面的数据渲染,这时候就需要事件处理,手势滑动到哪里,就展示那块的数据(比如鼠标知道EFGH,这时候就只能展示EFGH字母开头的数据)。前面做了那么多工作,这里就很好解决了,这里的`cityListKey`本身就是从分好组的数据里面提取的,所以知道下标就可以得到想要的数据了。 285 | 286 | ![VUE开发一个组件——Vue PC城市选择](http://cdn.javanx.cn/wp-content/themes/lensnews2.2/images/post/20181127155716.png) 287 | 288 | 所以`upCityListKey`方法传入`index`下标。来取对应数据。 289 | 290 | ```javascript 291 | upCityListKey(index){ 292 | this.upCityListIndex = index; 293 | this.upCityList = this.cityList[index]; 294 | } 295 | ``` 296 | 297 | 这里用`upCityListIndex`存入下标,用来添加鼠标划入的高亮样式,用`upCityList`存需要展示的城市数据。然后将`upCityList`渲染到页面 298 | 299 | ```html 300 |
301 | 304 |
305 | 309 |
310 |
311 | ``` 312 | 313 | ```scss 314 | .city-content{ 315 | max-height: 500px; 316 | overflow-y: auto; 317 | overflow-x: hidden; 318 | padding: 10px 13px 0 13px; 319 | label{ 320 | display: block; 321 | margin-bottom: 5px !important; 322 | font-size: 20px !important; 323 | margin-left: 0 !important; 324 | color: #5f5f5f !important; 325 | margin-top: 5px; 326 | } 327 | li{ 328 | padding: 6px 0 6px; 329 | float: left; 330 | text-align: left; 331 | font-size: 14px; 332 | min-width: 56px; 333 | margin-right: 24px; 334 | cursor: pointer; 335 | } 336 | } 337 | ``` 338 | 339 | 同时去掉`..city-components`的`height`样式。 340 | ```scss 341 | .city-components{ 342 | position: absolute; 343 | width: 400px; 344 | // height: 200px; 345 | box-shadow: 0 0 4px 0 rgba(117,117,117,0.5); 346 | border-radius: 2px; 347 | padding: 20px 21px; 348 | } 349 | ``` 350 | 351 | 完成效果 352 | 353 | ![VUE开发一个组件——Vue PC城市选择](http://cdn.javanx.cn/wp-content/themes/lensnews2.2/images/post/20181127160859.gif) 354 | 355 | 源码地址: [vue-c-city](https://github.com/javanf/vue-c-city) 356 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-c-city 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-c-city", 3 | "description": "A Vue.js project", 4 | "version": "1.0.0", 5 | "author": "Javanx(www.javanx.cn)", 6 | "license": "MIT", 7 | "private": true, 8 | "scripts": { 9 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", 10 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" 11 | }, 12 | "dependencies": { 13 | "vue": "^2.5.11" 14 | }, 15 | "browserslist": [ 16 | "> 1%", 17 | "last 2 versions", 18 | "not ie <= 8" 19 | ], 20 | "devDependencies": { 21 | "babel-core": "^6.26.0", 22 | "babel-loader": "^7.1.2", 23 | "babel-preset-env": "^1.6.0", 24 | "babel-preset-stage-3": "^6.24.1", 25 | "cross-env": "^5.0.5", 26 | "css-loader": "^0.28.7", 27 | "file-loader": "^1.1.4", 28 | "node-sass": "^4.5.3", 29 | "sass-loader": "^6.0.6", 30 | "vue-loader": "^13.0.5", 31 | "vue-template-compiler": "^2.4.4", 32 | "webpack": "^3.6.0", 33 | "webpack-dev-server": "^2.9.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javanf/vue-c-city/5328e962aebb9d972bd4e5cc5ebf7f1ef757d543/src/assets/logo.png -------------------------------------------------------------------------------- /src/h5.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | #app{ 2 | background: #efefef; 3 | } 4 | *{ 5 | margin: 0; 6 | padding: 0; 7 | } 8 | .city-wap{ 9 | color: #3b4f62; 10 | 11 | .clearfix{ 12 | &:after{ 13 | content: ''; 14 | display: block; 15 | clear: both; 16 | } 17 | } 18 | p{ 19 | background: #fff; 20 | margin-bottom: 10px; 21 | padding: 0 12px; 22 | } 23 | .search{ 24 | position: fixed; 25 | top: 0; 26 | box-shadow: 0 1px 3px 0 rgba(59,79,98,0.1); 27 | width: 100%; 28 | height: 50px; 29 | input{ 30 | line-height: 50px; 31 | width: 100%; 32 | border: none; 33 | box-shadow: none; 34 | padding: 0 10px; 35 | &:focus { 36 | outline: none; 37 | } 38 | } 39 | } 40 | .city-list{ 41 | .block-60{ 42 | height: 60px; 43 | } 44 | ul{ 45 | padding: 0 10px; 46 | li{ 47 | list-style: none; 48 | display: inline-block; 49 | margin-right: 10px; 50 | width: 29%; 51 | margin-bottom: 8px; 52 | line-height: 35px; 53 | text-align: center; 54 | color: #333; 55 | border-radius: 3px; 56 | background: #fff; 57 | font-size: 14px; 58 | white-space: nowrap; 59 | overflow: hidden; 60 | text-overflow: ellipsis; 61 | padding: 0 2px; 62 | } 63 | } 64 | } 65 | .filter{ 66 | position: fixed; 67 | right: 3px; 68 | top: 60px; 69 | font-size: 15px; 70 | div{ 71 | margin-top: 2px; 72 | text-align: center; 73 | } 74 | } 75 | .active-key{ 76 | position: fixed; 77 | width: 100px; 78 | height: 100px; 79 | line-height: 100px; 80 | top: 50%; 81 | left: 50%; 82 | transform: translate(-50%, -50%); 83 | z-index: 100; 84 | background: #dedede; 85 | color: #fff; 86 | border-radius: 100%; 87 | text-align: center; 88 | font-size: 40px; 89 | } 90 | } 91 | .city{ 92 | position:relative; 93 | .city-components{ 94 | position: absolute; 95 | width: 500px; 96 | box-shadow: 0 0 4px 0 rgba(117,117,117,0.5); 97 | border-radius: 2px; 98 | padding: 20px 21px; 99 | .clearfix{ 100 | &:after{ 101 | content: ''; 102 | display: block; 103 | clear: both; 104 | } 105 | } 106 | li{ 107 | list-style: none; 108 | } 109 | ul{ 110 | padding: 0; 111 | margin: 0; 112 | } 113 | .filter-tabar{ 114 | border-bottom: 1px solid #d7d7d7; 115 | cursor: pointer; 116 | li{ 117 | text-align: center; 118 | padding: 0 14px; 119 | float: left; 120 | padding-bottom: 14px; 121 | font-size: 14px; 122 | margin: 0 8px; 123 | margin-bottom: -1px; 124 | position: relative; 125 | &.active{ 126 | border-bottom: 1px solid #ff7362; 127 | } 128 | } 129 | } 130 | .city-content{ 131 | max-height: 500px; 132 | overflow-y: auto; 133 | overflow-x: hidden; 134 | padding: 10px 13px 0 13px; 135 | label{ 136 | display: block; 137 | margin-bottom: 5px !important; 138 | font-size: 20px !important; 139 | margin-left: 0 !important; 140 | color: #5f5f5f !important; 141 | margin-top: 5px; 142 | } 143 | li{ 144 | padding: 6px 0 6px; 145 | float: left; 146 | text-align: left; 147 | font-size: 14px; 148 | min-width: 56px; 149 | margin-right: 24px; 150 | cursor: pointer; 151 | } 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | // import App from './App.vue' 3 | import App from './h5.vue' 4 | 5 | new Vue({ 6 | el: '#app', 7 | render: h => h(App) 8 | }) 9 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | entry: './src/main.js', 6 | output: { 7 | path: path.resolve(__dirname, './dist'), 8 | publicPath: '/dist/', 9 | filename: 'build.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.css$/, 15 | use: [ 16 | 'vue-style-loader', 17 | 'css-loader' 18 | ], 19 | }, 20 | { 21 | test: /\.scss$/, 22 | use: [ 23 | 'vue-style-loader', 24 | 'css-loader', 25 | 'sass-loader' 26 | ], 27 | }, 28 | { 29 | test: /\.sass$/, 30 | use: [ 31 | 'vue-style-loader', 32 | 'css-loader', 33 | 'sass-loader?indentedSyntax' 34 | ], 35 | }, 36 | { 37 | test: /\.vue$/, 38 | loader: 'vue-loader', 39 | options: { 40 | loaders: { 41 | // Since sass-loader (weirdly) has SCSS as its default parse mode, we map 42 | // the "scss" and "sass" values for the lang attribute to the right configs here. 43 | // other preprocessors should work out of the box, no loader config like this necessary. 44 | 'scss': [ 45 | 'vue-style-loader', 46 | 'css-loader', 47 | 'sass-loader' 48 | ], 49 | 'sass': [ 50 | 'vue-style-loader', 51 | 'css-loader', 52 | 'sass-loader?indentedSyntax' 53 | ] 54 | } 55 | // other vue-loader options go here 56 | } 57 | }, 58 | { 59 | test: /\.js$/, 60 | loader: 'babel-loader', 61 | exclude: /node_modules/ 62 | }, 63 | { 64 | test: /\.(png|jpg|gif|svg)$/, 65 | loader: 'file-loader', 66 | options: { 67 | name: '[name].[ext]?[hash]' 68 | } 69 | } 70 | ] 71 | }, 72 | resolve: { 73 | alias: { 74 | 'vue$': 'vue/dist/vue.esm.js' 75 | }, 76 | extensions: ['*', '.js', '.vue', '.json'] 77 | }, 78 | devServer: { 79 | historyApiFallback: true, 80 | noInfo: true, 81 | overlay: true 82 | }, 83 | performance: { 84 | hints: false 85 | }, 86 | devtool: '#eval-source-map' 87 | } 88 | 89 | if (process.env.NODE_ENV === 'production') { 90 | module.exports.devtool = '#source-map' 91 | // http://vue-loader.vuejs.org/en/workflow/production.html 92 | module.exports.plugins = (module.exports.plugins || []).concat([ 93 | new webpack.DefinePlugin({ 94 | 'process.env': { 95 | NODE_ENV: '"production"' 96 | } 97 | }), 98 | new webpack.optimize.UglifyJsPlugin({ 99 | sourceMap: true, 100 | compress: { 101 | warnings: false 102 | } 103 | }), 104 | new webpack.LoaderOptionsPlugin({ 105 | minimize: true 106 | }) 107 | ]) 108 | } 109 | --------------------------------------------------------------------------------