美文网首页工作生活
angular8旅游清单项目

angular8旅游清单项目

作者: 云翼飞 | 来源:发表于2019-07-03 14:17 被阅读0次

    TourismTodo(旅游清单)

    前言

    本文的目的是通过一步步实现一个旅游清单项目,让大家快速入门Angular8以及百度地图API。我们将收获:

    • Angular8基本用法,架构
    • 使用百度地图API实现自己的地图应用
    • 解决调用百度地图API时的跨域问题
    • 对localStorage进行基础封装,进行数据持久化
    • Ant Design UI的使用,官网
      首页.png
      我的大陆.png

    项目简介

    一个想法,可以有这样一个程序,记录自己的路途,见闻和感想。项目的首页展示的是已去过的旅游地点和路线,地图路线是通过调用百度地图api实现的,当然提供这样的api很多,大家可以根据自己的喜好去使用。其次我们可以在首页添加未来的旅游规划和预算,方便后面使用。我的大陆页面主要展示的你去过的和即将要去的路线,可以进行相关操作。

    功能实现

    1. 项目首页展示已经去过的旅游地点和路线,地图路线由百度api实现。
    2. 在首页添加未来的旅游规划和预算表单
    3. 我的大陆展示你去过的和即将要去的路线,可以进行相关操作。

    项目地址

    基于angular8和百度地图API开发旅游清单项目

    1.开始

    1. 安装脚手架
      npm install -g @angular/cli
    2. 创建工作空间和初始应用
      ng new my-app
    3. 根据以上架构,建立对应目录文件
    4. 启动服务
    cd my-app
    ng serve --open
    

    这里cli会自动打开浏览器4200端口,并出现默认页面。

    2.引入百度地图API

    官方会提供不同地图功能的api地址,以下是该项目使用的地址:

    <script type="text/javascript" src="http://api.map.baidu.com/api?v=2.0&ak=你的ak"></script>
    <script type="text/javascript" src="http://api.map.baidu.com/library/CurveLine/1.5/src/CurveLine.min.js"></script>
    

    如果没有ak,请移步百度地图官网申请,步骤也很简单。

    3.关于angular

    一个完整的angular应该包括:

    1. 模块
    • Angular 定义了 NgModule,NgModule 为一个组件集声明了编译的上下文环境,它专注于某个应用领域、某个工作流或一组紧密相关的能力,每个 Angular 应用都有一个根模块,通常命名为 AppModule。根模块提供了用来启动应用的引导机制。 一个应用通常会包含很多功能模块。
    1. 组件
    • 每个 Angular 应用都至少有一个组件,也就是根组件,它会把组件树和页面中的 DOM 连接起来。 每个组件都会定义一个类,其中包含应用的数据和逻辑,并与一个 HTML 模板相关联,该模板定义了一个供目标环境下显示的视图 比如:
    import { Component, OnInit } from '@angular/core';
    import { LocationService } from '../../service/list';
    
    @Component({
      selector: 'app-bar',
      templateUrl: './index.html',
      styleUrls: ['./index.scss']
    })
    export class AppBar implements OnInit {
        items;
        constructor(private locationService: LocationService) {
          this.items = this.locationService.getItems();
        }
    
        ngOnInit() {
    
        }
    }
    
    1. 服务与依赖注入
    • 对于与特定视图无关并希望跨组件共享的数据或逻辑,可以创建服务类。 服务类的定义通常紧跟在 “@Injectable()” 装饰器之后。该装饰器提供的元数据可以让你的服务作为依赖被注入到客户组件中。例如:
    import { Injectable } from '@angular/core';
    
    @Injectable({
        providedIn: 'root'
      })
    export class Storage {}
    
    1. 路由
    • Angular 的 Router 模块提供了一个服务,它可以让你定义在应用的各个不同状态和视图层次结构之间导航时要使用的路径。如下:
    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    
    import { HomeComponent } from './home';
    import { NewMapComponent } from './newMap';
    // 路由不能以‘/’开始
    const routes: Routes = [
      //匹配空路由时加载的组件
      { path: '', component: HomeComponent },
      { path: 'newMap', component: NewMapComponent },
      //匹配不到路由的时候加载的组件 或者跳转的路由
      {path: '**', redirectTo:'home'},  
    ];
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule]
    })
    export class AppRoutingModule { }
    

    4,百度地图api及跨域问题解决

    我们进入百度地图官网后,去控制台创建一个应用,此时会生成对应的应用ak,如下:


    创建百度地图应用
    • 本地调试时将referer写成*即可,但是我们用ng的http或者fetch去请求api接口时仍会出现跨域,在网上搜集了各种资料,都没有达到效果,我们这里使用jquery的$.getScript(url),结合jsonp回调,即可解决该问题。

    • 所以先安装以下jquery:
      npm install jquery

    • 解决方案如下:

    1. 封装http服务:
    import { Injectable } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { AK, BASE_URL } from '../config';
    import * as $ from "jquery";
    
    @Injectable({
        providedIn: 'root'
      })
    export class Http {
        constructor(
            private http: HttpClient
        ) {}
    
        params(data = {}) {
            let obj = {...data, ak: AK, output: 'json' };
            let paramsStr = '?';
            for(let v in obj) {
                paramsStr += `${v}=${obj[v]}&`
            };
            return paramsStr.substr(0, paramsStr.length -1);
        }
    
        get(url, params) {
            return this.http.get(`${BASE_URL}${url}${this.params(params)}`)
        }
    
        getCors(url, params) {
            return new Promise((resolve, reject) => {
                $.getScript(`${BASE_URL}${url}${this.params(params)}`, (res, status) => {
                    if(status === 'success') {
                        resolve(status)
                    } else {
                        reject(status)
                    }  
                });
            })
            
        }
    }
    
    1. 定义jsonp回调和接收数据变量:
    // home.component.ts
    //全局定义callback函数,获取请求数据
    let locationData = null;
    window['cb'] = function(data) {
      locationData = data && data.results;
    }
    
    1. 使用:
    // home.component.ts
    async searchLocation(v) {
      return await this.http.getCors('/place/v2/search',
      { region:v, query: v, callback: 'cb' });
    }
    
    this.searchLocation(currentData.name).then(res => {
      console.log(locationData) //返回的数据
    })
    

    5.引入Ant Design

    1. 执行以下命令后将自动完成 ng-zorro-antd 的初始化配置,包括引入国际化文件,导入模块,引入样式文件等工作。
      ng add ng-zorro-antd
    2. 布局入口页面
    • 主体页面整体分为三部分,头部(app-bar),内容(页面路由),底部(app-footer),
    <div class="app-wrap">
      <app-bar></app-bar>
      <main class="main">
        <router-outlet></router-outlet>
      </main>
      <app-footer></app-footer>
    </div>
    
    1. 定义页头页尾组件
    // app-bar.component.html
    <div class="bar-warp">
      <div class="logo">旅游导图</div>
      <a [routerLink]="[ '/home' ]" routerLinkActive="active">首页</a>
      <nz-badge [nzCount]="items.length" class="badge">
          <a [routerLink]="[ '/newmap' ]" routerLinkActive="active">我的大陆</a>
      </nz-badge>
    </div>
    //app-bar.component.ts
    import { Component, OnInit } from '@angular/core';
    import { LocationService } from "../../services/location/location.service";
    @Component({
      selector: 'app-bar',
      templateUrl: './app-bar.component.html',
      styleUrls: ['./app-bar.component.scss']
    })
    export class AppBarComponent implements OnInit {
      items;
    
      constructor(private location:LocationService) {
        this.items = this.location.getItems();
       }
    
      ngOnInit() {
      }
    
    }
    
    //footer.component.html
    <footer class="footer">@开发者:{{name}}</footer>
    //footer.component.ts
    import { Component, OnInit } from '@angular/core';
    
    @Component({
      selector: 'app-footer',
      templateUrl: './footer.component.html',
      styleUrls: ['./footer.component.scss']
    })
    export class FooterComponent implements OnInit {
      name = '云翼飞'
      constructor() { }
    
      ngOnInit() {
      }
    }
    

    scss请看源代码学习:基于angular8和百度地图API开发旅游清单项目

    6.页面头部组件用到了LocationService,我们来看看这个service:

    import { Injectable } from '@angular/core';
    import { HttpClient } from "@angular/common/http";
    import { StorageService } from "../storage/storage.service";
    import { NzMessageService } from 'ng-zorro-antd';
    /**
     * 访问列表,添加旅游清单,清除清单功能
     *
     * @export
     * @class LocationService
     */
    @Injectable({
      providedIn: 'root'
    })
    export class LocationService {
      items = [
        {
          name: '北京',
          desc: '北京好,风景真的不错!',
          price: '2000',
          date: 'Fri Jul 05 2019 09:35:37 GMT+0800 (中国标准时间)',
          hasDone: true,
          location: {
            lat: 39.910924,
            lng: 116.413387
          }
        },
        {
          name: '苏州',
          desc: '苏州好,去了还想去,不错!',
          price: '2000',
          hasDone: true,
          date: '1562204417083',
          location: { 
            lat: 31.303565,
            lng: 120.592412
          }
        },
        {
          name: '上海',
          desc: '上海好,去了还想去,不错!',
          price: '2000',
          hasDone: true,
          date: '2018-12-29',
          location: { 
            lat: 31.235929, 
            lng: 121.48054 
          }
        },
        {
          name: '武汉',
          desc: '武汉好,去了还想去,不错!',
          price: '2000',
          hasDone: false,
          date: '2018-12-29',
          location: { 
            lat: 30.598467,
            lng: 114.311586
          }
        }
      ];
      constructor(private http:HttpClient,
        private storage:StorageService,
        private message:NzMessageService
        ) { 
          if(storage.get('list')) {
            this.items = storage.get('list')
          }
        }
    
        getItems() {
          return this.items;
        }
    
        addToList(data) {
          // console.log(data)
          this.items.push(data);
          this.storage.set('list',this.items)
          this.message.success('添加成功');
        }
    
        clearList() {
          this.items = [];
          return this.items;
        }
    }
    
    • 该服务主要提供访问列表getItems(),添加旅游清单(),清除清单clearList()的功能,我们利用@Injectable({ providedIn: 'root' })将服务注入根组件以便共享服务。其次我们使用自己封装的Storage服务来进行持久化数据存储,storage服务如下:
    //storage.service.ts
    import { Injectable } from '@angular/core';
    
    @Injectable({
      providedIn: 'root'
    })
    export class StorageService {
    
      constructor() { }
      set(key:string,value:any) {
        localStorage.setItem(key,JSON.stringify(value));
      }
      get(key:string) {
        return JSON.parse(localStorage.getItem(key));
      }
      remove(key:string) {
        localStorage.removeItem(key);
      }
    }
    

    7.核心功能百度地图实现

    // home.component.ts
    import { Component, OnInit } from '@angular/core';
    import { FormBuilder, FormGroup, Validators } from '@angular/forms';
    import { LocationService } from "../../services/location/location.service";
    import { RequestService } from "../../services/request/request.service";
    import { HttpClient } from "@angular/common/http";
    import { NzMessageService } from 'ng-zorro-antd';
    
    //声明变量
    declare var BMap: any;
    declare var BMapLib: any;
    
    //全局定义callback函数,获取请求数据
    let locationData = null;
    window['cb'] = function(data) {
      locationData = data && data.results;
    }
    @Component({
      selector: 'app-home',
      templateUrl: './home.component.html',
      styleUrls: ['./home.component.scss']
    })
    export class HomeComponent implements OnInit {
      hasDoneList;
      validateForm: FormGroup;
      constructor(private fb: FormBuilder,
        private location:LocationService,
        private request:RequestService,
        private http:HttpClient,
        private message:NzMessageService
        ) {
          this.hasDoneList = this.location.getItems().filter(item => {
            return item.hasDone && item
          });
          this.validateForm = this.fb.group({
            name: [null, [Validators.required]],
            price: [null, [Validators.required]],
            date: [null, [Validators.required]],
          });
      }
    
      ngOnInit() {
        
        // 百度地图API功能
        var map = new BMap.Map("baidu-map");          // 创建地图实例  
        map.centerAndZoom(new BMap.Point(118.454, 32.955), 6);  // 创建点坐标  
        map.enableScrollWheelZoom();
        let points = [];
        this.location.getItems().forEach(item => 
          // && 只要&&前面为true,无论&&后面是true还是false,结果都返回&&后面的值
          item.hasDone &&  points.push(new BMap.Point(item.location.lng,item.location.lat))
        )
    
        var curve = new BMapLib.CurveLine(points, { strokeColor: "blue", strokeWeight: 3, strokeOpacity: 0.5 }); //创建弧线对象
        map.addOverlay(curve); //添加到地图中
        curve.enableEditing(); //开启编辑功能
      }
    
      async searchLocation(v) {
        return await this.request.getCors('/place/v2/search',
        {region: v, query: v, callback: 'cb'});
      }
    
      submitForm(): void {
        for (const i in this.validateForm.controls) {
          this.validateForm.controls[i].markAsDirty();
          this.validateForm.controls[i].updateValueAndValidity();
        }
        let currentData = this.validateForm.value; //重新赋值
        let date = new Date(currentData.date);
        currentData.date = date.getTime();
        this.searchLocation(currentData.name).then(res => {
          if(!locationData.length) {
            this.message.error('没有找到这个城市');
            return false
          }
          this.location.addToList({
            ...currentData,
            location: locationData[0].location,
            desc: '计划中...',
            hasDone: false
          })
          
        });
        this.validateForm.reset();
        
      }
      resetForm(e: MouseEvent): void {
        e.preventDefault();
        this.validateForm.reset();
        for (const key in this.validateForm.controls) {
          this.validateForm.controls[key].markAsPristine();
          this.validateForm.controls[key].updateValueAndValidity();
        }
      }
    
    }
    
    <!-- home.component.html -->
    <div class="home-warp">
      <section class="content-list">
        <div class="content-item">
          <nz-divider nzText="我已去过" nzOrientation="left"></nz-divider>
          <div class="visit-list">
            <div nz-row>
              <div class="list-button" nz-col nzSpan="6" *ngFor="let item of hasDoneList">
                <button nz-button nz-popover nzType="primary" [nzContent]="item.desc" nzPlacement="bottom">
                  {{item.name}}
                </button>
              </div>
            </div>
          </div>
        </div>
        <div class="content-item">
          <nz-divider nzText="未来规划" nzOrientation="left"></nz-divider>
          <div class="future-list">
            <form nz-form [formGroup]="validateForm" class="login-form" (ngSubmit)="submitForm()">
              <nz-form-item>
                <nz-form-label [nzSm]="6" [nzXs]="24" nzFor="name" nzRequired>地点</nz-form-label>
                <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="请输入城市名字">
                  <input nz-input id="name" formControlName="name" placeholder="城市名字" />
                </nz-form-control>
              </nz-form-item>
    
              <nz-form-item>
                <nz-form-label [nzSm]="6" [nzXs]="24" nzFor="price" nzRequired>预算</nz-form-label>
                <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="请输入预算">
                  <input type="number" nz-input id="price" formControlName="price" placeholder="预算" />
                </nz-form-control>
              </nz-form-item>
              <nz-form-item>
                <nz-form-label [nzSm]="6" [nzXs]="24" nzFor="price" nzRequired>日期</nz-form-label>
                <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="请填写日期">
                  <nz-date-picker formControlName="date"></nz-date-picker>
                </nz-form-control>
              </nz-form-item>
              <nz-form-item nz-row class="register-area">
                <nz-form-control [nzSpan]="14" [nzOffset]="6">
                  <button nz-button class="login-form-button" [nzType]="'primary'" style="margin-right:20px;">提交</button>
                  <button nz-button nzType="primary" (click)="resetForm($event)">重置</button>
                </nz-form-control>
              </nz-form-item>
            </form>
          </div>
        </div>
      </section>
      <section class="map-warp" id="baidu-map"></section>
    </div>
    
    1. 在ngOninit生命周期里,初始化地图数据,根据前面我们定义的list server,把hasDone为true的数据过滤出来,显示在地图上。
    2. 在constructor构造函数中,初始化已经去过的数据,把hasDone为true的数据过滤出来,显示在页面上。
    3. 使用ant Design的表单实现添加旅游清单
    4. 在提交表单之前,先调用百度地图的api生成城市对应的经纬度,然后一起添加到清单中,这样做的目的是想要画路线图,我们需要给百度api提供经纬度数据,如果没有经纬度数据则添加清单失败,
    5. 由于涉及到跨域,我们需要定义jsonp的回调,拿到数据,如下:
    let locationData = null;
    window['cb'] = function(data) {
      locationData = data && data.results;
    }
    
    1. locationService的addToList方法会将数据添加到清单,并存储到storage中。 如果想了解完整代码,欢迎在我的github上查看。

    8.我的大陆页面

    • 其涉及的难点不是很多,主要是根据hasDone为true或false去显示不同的样式。
    <div class="detail-warp">
      <h1>新大陆</h1>
      <div class="detail-list" >
        <div *ngFor="let item of list" style="margin: 10px">
          <nz-card class="position-item" style="width:300px;" 
          [nzExtra]="new"  [nzTitle]="item.name"
            [nzExtra]="extraTemplate">
            <p>{{item.date | date:'yyyy-MM-dd'}}</p>
            <p>{{item.desc}}</p>
            <p class="price">预算:{{item.price}}</p>
          </nz-card>
          <ng-template #new >
            <strong *ngIf="!item.hasDone" class="card-new">新</strong>
          </ng-template>
        </div>
      </div>
    </div>
    
    import { Component, OnInit } from '@angular/core';
    import { LocationService } from "../../services/location/location.service";
    @Component({
      selector: 'app-new-map',
      templateUrl: './new-map.component.html',
      styleUrls: ['./new-map.component.scss']
    })
    export class NewMapComponent implements OnInit {
      list;
      
      constructor(private location:LocationService) { 
        this.list = location.getItems();
      }
    
      ngOnInit() {
      }
    }
    

    总结

    • 该项目是基于angular8的实战入门项目,涉及到部分高级技巧以及百度地图,jsonp跨域的知识,
    • 待完成部分:我的大陆页面卡片添加编辑功能,编辑是否已经去过此地以及备注等

    错误

    1. Can't bind to 'formGroup' since it isn't a known property of 'form'.
    • 解决方案:需要从@angular/forms导入ReactiveFormsModule。因为FormGroupDirective 指令属于ReactiveFormsModule一部分。
    • 在app.module.ts导入ReactiveFormsModule模块
    • import { FormsModule, ReactiveFormsModule } from '@angular/forms';
    1. Uncaught TypeError: Cannot read property 'gc' of undefined
    • 问题原因:百度地图,错误,页面上缺少渲染地图的div元素
    • 解决方案:页面初始化一个绑定id值的div
    1. ERROR in src/app/home/index.ts(38,21): error TS2552: Cannot find name 'BMap'. Did you mean 'map'?
      src/app/home/index.ts(40,29): error TS2552: Cannot find name 'BMap'. Did you mean 'map'?
      src/app/home/index.ts(44,51): error TS2552: Cannot find name 'BMap'. Did you mean 'map'?
      src/app/home/index.ts(47,23): error TS2304: Cannot find name 'BMapLib'.
    • 问题原因:未定义map,BMapLib变量,
    • 在使用前声明变量。
    declare var BMap: any;
    declare var BMapLib: any;
    

    参考:

    使用Angular8和百度地图api开发《旅游清单》

    相关文章

      网友评论

        本文标题:angular8旅游清单项目

        本文链接:https://www.haomeiwen.com/subject/uzkqhctx.html