├── .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 | 
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 |
22 |
23 |
{{title}}
24 |
25 |
26 |
27 | 城市控件
28 |
29 |
30 |
31 |
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 | 
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 | 
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 | 
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 | 
237 |
238 | 终于数据是我们要的了,这时候直接将数据渲染到页面即可。(当然如果后台能直接给你这样的数据结构,你就感觉感谢吧)
239 |
240 | ### 数据渲染
241 | ```html
242 |
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 | 
283 |
284 | Ok,继续把下面的数据渲染,这时候就需要事件处理,手势滑动到哪里,就展示那块的数据(比如鼠标知道EFGH,这时候就只能展示EFGH字母开头的数据)。前面做了那么多工作,这里就很好解决了,这里的`cityListKey`本身就是从分好组的数据里面提取的,所以知道下标就可以得到想要的数据了。
285 |
286 | 
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 |
306 |
307 | - {{ritem.airportName}}
308 |
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 | 
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 |
2 |
3 |
{{title}}
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 | - {{ritem.airportName}}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/javanf/vue-c-city/5328e962aebb9d972bd4e5cc5ebf7f1ef757d543/src/assets/logo.png
--------------------------------------------------------------------------------
/src/h5.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
{{item.ckey}}
11 |
12 | - {{ritem.airportName}}
13 |
14 |
15 |
16 |
19 |
{{activeKey}}
20 |
21 |
22 |
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 |
--------------------------------------------------------------------------------