iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 30
1
Blockchain

區塊練起來-智能合約與DApp開發系列 第 30

[區塊練起來-智能合約與DApp開發] DAY 30 - 實戰DApp!區塊鏈履歷應用(4)

  • 分享至 

  • xImage
  •  

貼心小語

上一篇完成了教育單位與企業單位的功能頁面,今天將會設計簡單的履歷頁面來呈現履歷資訊,衝呀~~~!!!


流程思考

我們當初在設計履歷合約的時候,有製作取得陣列長度的函式,為的就是讓外部能夠得知總共有幾筆資料,進而根據陣列長度一一取出資訊。所以我們得知了 必須要先知道有幾筆相關資料,再透過迴圈取出資訊 ,需要注意的是修課證明與證照資訊需要根據學歷來取出相對應的資訊,所以 必須針對每一個學歷來取出修課證明與證照資訊的陣列長度,再用迴圈去取得所有相關資訊

根據上面的敘述我們可以規劃取資料的流程:

  1. 先將 Profile 的資訊取出來
  2. 取出 EducationExperience 以及 Skill 的陣列長度
  3. 根據 Education 的陣列長度來取出所有的 Education 資訊
  4. 根據 Education 的陣列長度來取出每一個學歷下的 Course 陣列長度
  5. 根據 Education 的陣列長度與 Course 的陣列長度來取出所有對應的 Course 資訊
  6. 根據 Education 的陣列長度來取出每一個學歷下的 License 陣列長度
  7. 根據 Education 的陣列長度與 License 的陣列長度來取出所有對應的 License 資訊
  8. 根據 Experience 的陣列長度來取出所有的 Experience 資訊
  9. 根據 Skill 的陣列長度來取出所有的 Skill 資訊

我們可以透過設計 Model 的方式讓 Component 的程式碼不會過於複雜,而且能夠把程式碼理得比較乾淨,所以預期會製作 ProfileModel 來處理上面大部分的操作,並將資訊記錄在 Model 上。

設計 Model

我們新增一個 profile.ts 在 types 資料夾中,並建立 ProfileModel :

export class ProfileModel {
}

將履歷表中該有的資訊設計進去,而需要取得陣列長度的資訊就另外設置 count 來存放筆數:

export class ProfileModel {
    resume: any; // 將履歷合約的實例存在這裡

    name: string;
    age: number;
    gender: string;
    account: string;
    contact: string;
    autobiography: string;

    educations: {
        count: number;
        items: Array<Education>;
    } = { count: 0, items: [] };

    experiences: {
        count: number;
        items: Array<Experience>;
    } = { count: 0, items: [] };

    skills: {
        count: number;
        items: Array<Skill>;
    } = { count: 0, items: [] };
}

export interface Education {
    schoolName: string;
    status: string;
    major: string;
    courses: {
        count: number;
        items: Array<Course>;
    };
    licenses: {
        count: number;
        items: Array<License>;
    };
}

export interface Experience {
    companyName: string;
    position: string;
    startDate: string;
    endDate: string;
}

export interface Skill {
    class: string;
    name: string;
}

export interface Course {
    courseName: string;
    content: string;
    comment: string;
    grade: number;
}

export interface License {
    licenseName: string;
    content: string;
}

設置 Profile 資訊

我們在建構 ProfileModel 實例的時候就設置 Profile 的資訊:

constructor(resume: any) {
    this.resume = resume;
    this.setBasic();
}

public setBasic(): void {
    from(this.resume.methods.profile().call()).pipe(take(1)).subscribe((basic: any) => {
        this.name = basic.name;
        this.age = basic.age;
        this.gender = this.transGender(Number(basic.gender));
        this.account = basic.account;
        this.contact = basic.contact;
        this.autobiography = basic.autobiography;
    });
}

private transGender(status: number): string {
    let result = '';
    switch (status) {
        case Gender.male:
            result = '男';
            break;
        case Gender.female:
            result = '女';
            break;
        case Gender.other:
            result = '其他';
            break;
    }
    return result;
}

設置學歷、經歷及技能的數量

設計一個設置 EducationExperience 以及 Skill 數量的方法:

public setCounts(counts: Array<string>): void {
    this.educations.count = Number(counts[0]);
    this.experiences.count = Number(counts[1]);
    this.skills.count = Number(counts[2]);
}

這裡可以選擇在內部做呼叫合約取得 count 或是從外部傳進來,我這邊先寫一個從外部傳進來的方法

設置學歷相關資訊

取得數量以後就根據 Education 的數量來一一取出學歷資料,再順便取得每一個學歷下的修課證明與證照:

public setEducations(): Observable<any> {
    const count = this.educations.count;
    const reqAry = [];

    if (!count) { return; }

    for (let i = 0; i < count; i++) {
        reqAry.push(from(this.resume.methods.getEducation(i).call()));
    }

    return forkJoin(reqAry).pipe(
        switchMap(res => {
            for (const item of res) {
                this.educations.items.push({
                    schoolName: item['0'],
                    status: this.transEducationStatus(Number(item['1'])),
                    major: item['2']
                } as Education);
            }
            return this.setCourseCount();
        }),
        switchMap(() => this.setCourses()),
        switchMap(() => this.setLicenseCount()),
        switchMap(() => this.setLicenses())
    );
}

private setCourseCount(): Observable<any> {
    const educationCount = this.educations.count;
    const reqAry = [];

    for (let i = 0; i < educationCount; i++) {
        reqAry.push(from(this.resume.methods.getCourseCount(i).call()));
    }

    return new Observable(observer => {
        forkJoin(reqAry).pipe(take(1)).subscribe(counts => {
            for (let i = 0; i < educationCount; i++) {
                // 因為在建立學歷的時候有建立一筆空資料的修課證明,所以要扣除該筆
                this.educations.items[i].courses = {
                    count: (Number(counts[i]) === 1) ? 0 : Number(counts[i]) - 1,
                    items: []
                };
            }
            observer.next();
            observer.complete();
        });
    });
}

private setLicenseCount(): Observable<any> {
    const educationCount = this.educations.count;
    const reqAry = [];

    for (let i = 0; i < educationCount; i++) {
        reqAry.push(from(this.resume.methods.getLicenseCount(i).call()));
    }

    return new Observable(observer => {
        forkJoin(reqAry).pipe(take(1)).subscribe(counts => {
            for (let i = 0; i < educationCount; i++) {
                // 因為在建立學歷的時候有建立一筆空資料的證照,所以要扣除該筆
                this.educations.items[i].licenses = {
                    count: (Number(counts[i]) === 1) ? 0 : Number(counts[i]) - 1,
                    items: []
                };
            }
            observer.next();
            observer.complete();
        });
    });
}

private setCourses(): Observable<any> {
    const educationCount = this.educations.count;
    const forkAry = [];

    for (let i = 0; i < educationCount; i++) {
        const courseCount = this.educations.items[i].courses.count;
        const reqAry = [];
        for (let j = 1; j <= courseCount; j++) {
            reqAry.push(from(this.resume.methods.getCourse(i, j).call()));
        }
        if (reqAry.length > 0) {
            forkAry.push(forkJoin(reqAry));
        }
    }

    return new Observable(observer => {
        forkJoin(forkAry).pipe(take(1)).subscribe(res => {
            for (let i = 0; i < educationCount; i++) {
                const courseCount = this.educations.items[i].courses.count;
                for (let j = 0; j < courseCount; j++) {
                    this.educations.items[i].courses.items.push({
                        courseName: res[i][j]['0'],
                        content: res[i][j]['1'],
                        comment: res[i][j]['2'],
                        grade: res[i][j]['3']
                    } as Course);
                }
            }
            observer.next();
            observer.complete();
        });
    });
}

private setLicenses(): Observable<any> {
    const educationCount = this.educations.count;
    const forkAry = [];

    for (let i = 0; i < educationCount; i++) {
        const licenseCount = this.educations.items[i].licenses.count;
        const reqAry = [];
        for (let j = 1; j <= licenseCount; j++) {
            reqAry.push(from(this.resume.methods.getLicense(i, j).call()));
        }
        if (reqAry.length > 0) {
            forkAry.push(forkJoin(reqAry));
        }
    }

    return new Observable(observer => {
        forkJoin(forkAry).pipe(take(1)).subscribe(res => {
            for (let i = 0; i < educationCount; i++) {
                const licenseCount = this.educations.items[i].licenses.count;
                for (let j = 0; j < licenseCount; j++) {
                    this.educations.items[i].licenses.items.push({
                        licenseName: res[i][j]['0'],
                        content: res[i][j]['1']
                    } as License);
                }
            }
            observer.next();
            observer.complete();
        });
    });
}

private transEducationStatus(status: number): string {
    let result = '';
    switch (status) {
        case EducationStatus.graduate:
            result = '畢業';
            break;
        case EducationStatus.learning:
            result = '在學中';
            break;
        case EducationStatus.undergraduate:
            result = '肄業';
            break;
    }
    return result;
}

設置經歷相關資訊

透過 Experience 的數量來一一取得經歷資料:

public setExperiences(): Observable<any> {
    const count = this.experiences.count;
    const reqAry = [];

    if (!count) { return; }

    for (let i = 0; i < count; i++) {
        reqAry.push(from(this.resume.methods.getExperience(i).call()));
    }

    return forkJoin(reqAry).pipe(
        map(res => {
            for (const item of res) {
                this.experiences.items.push({
                    companyName: item['0'],
                    position: item['1'],
                    startDate: this.transDate(Number(item['2'])),
                    endDate: this.transDate(Number(item['3']))
                } as Experience);
            }
            return;
        })
    );
}

private transDate(value: number): string {
    let result = '';
    if (!value) {
        result = '尚未設置';
    } else {
        const date = new Date(value);
        result = `${ date.getFullYear() }/${ date.getMonth() + 1 }/${ date.getDate() }`;
    }
    return result;
}

設置專業技術相關資訊

透過 Skill 的數量來一一取得專業技術:

public setSkills(): Observable<any> {
    const count = this.skills.count;
    const reqAry = [];

    if (!count) { return; }

    for (let i = 0; i < count; i++) {
        reqAry.push(from(this.resume.methods.getSkill(i).call()));
    }

    return forkJoin(reqAry).pipe(
        map(res => {
            for (const item of res) {
                this.skills.items.push({
                    class: item['0'],
                    name: item['1']
                } as Skill);
            }
            return;
        })
    );
}

在 Home 元件下呈現

我們將 ProfileModel 設計完畢後,就可以在 Home 元件來呈現履歷資料囉!首先我們先設計 home.component.ts ,需要注意的是我們要在前端做專業技能的分類,把相同類別的技能歸為一類,這樣會比較好閱讀:

import { Component, Injector } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { switchMap, take } from 'rxjs/operators';
import { forkJoin } from 'rxjs';
import { ComponentBase } from 'src/app/base/component.base';
import { ProfileModel } from 'src/app/types';

@Component({
    selector: 'app-home',
    templateUrl: './home.component.html',
    styleUrls: ['./home.component.scss']
})
export class HomeComponent extends ComponentBase {
    public contractForm: FormGroup;
    public profile: ProfileModel = null;
    public skills = null; // 存放歸類的專業技能

    public loaded = false; // 是否讀取完畢

    constructor(
        private injector: Injector,
        private formBuilder: FormBuilder
    ) {
        super(injector);
        this.contractForm = this.formBuilder.group({
            contract: ['', [Validators.required, this.addressValidator]]
        });
    }

    public getProfile(contract: string): void {
        const resume = this.providerSvc.getResume(contract);
        const countReq = [];

        this.loaded = false;
        this.profile = new ProfileModel(resume);

        countReq.push(
            this.providerSvc.executeMethod(resume.methods.getEducationCount().call()),
            this.providerSvc.executeMethod(resume.methods.getExperienceCount().call()),
            this.providerSvc.executeMethod(resume.methods.getSkillCount().call())
        );

        forkJoin(countReq).pipe(
            switchMap(res => {
                this.profile.setCounts(res);
                return this.profile.setEducations();
            }),
            switchMap(() => this.profile.setExperiences()),
            switchMap(() => this.profile.setSkills()),
            take(1)
        ).subscribe(() => {
            this.dealSkills();
            this.loaded = true;
        });
    }

    private dealSkills(): void {
        const skills = this.profile.skills.items;
        const classBuffer = skills.map(x => x.class); // 取出所有專業技能類別
        const noRepeatClass = classBuffer.filter((val, i, arr) => arr.indexOf(val) === i); // 篩出不重複的類別
        const sk = [];
        noRepeatClass.forEach(key => {
          sk.push({
              class: key,
              items: skills.filter(x => x.class === key).map(x => x.name)
          });
        });
        this.skills = sk;
    }

}

設計頁面:

<div class="container my-3">
    <form
        [formGroup]="contractForm"
        (ngSubmit)="getProfile(contractForm.value.contract)"
    >
        <div class="input-group mb-2 mr-sm-2">
            <input type="text" 
                class="form-control" 
                placeholder="輸入履歷位址" 
                [formControlName]="'contract'"
            >
            <button type="submit" 
                class="btn btn-primary" 
                [disabled]="contractForm.invalid">
                確認
            </button>
        </div>
    </form>
    <div class="card" *ngIf="loaded && profile">
        <div class="card-body">
            <h2 class="card-title">{{ profile.name }}</h2>
        </div>
        <ul class="list-group list-group-flush">
            <li class="list-group-item"><b>性別:</b>{{ profile.gender }}</li>
            <li class="list-group-item"><b>年齡:</b>{{ profile.age }}</li>
            <li class="list-group-item"><b>聯絡:</b>{{ profile.contact }}</li>
            <li class="list-group-item"><b>帳戶:</b>{{ profile.account }}</li>
        </ul>
        <div class="card-body">
            <h3>自傳</h3>
            <p>{{ profile.autobiography }}</p>
            <hr>
        </div>
        <div class="card-body">
            <h3>專業技能</h3>
            <div class="my-4" *ngFor="let skill of skills">
                <h4>{{ skill.class }}</h4>
                <p *ngFor="let item of skill.items">{{ item }}</p>
            </div>
        </div>
        <div class="card-body">
            <h3>學歷</h3>
            <div class="my-4" *ngFor="let edu of profile.educations.items">
                <h4>
                    {{ edu.schoolName }} 
                    <small class="text-muted">{{ edu.major }}</small>
                </h4>
                <p>學業狀態:{{ edu.status }}</p>
                <h5>修習課程:</h5>
                <div *ngFor="let course of edu.courses.items">
                    <p><b>{{ course.courseName }}</b></p>
                    <p>課程內容:{{ course.content }}</p>
                    <p>學期總分:{{ course.grade }}</p>
                    <p>教師評語:{{ course.comment }}</p>
                </div>
                <h5>證照:</h5>
                <div *ngFor="let license of edu.licenses.items">
                    <p><b>{{ license.licenseName }}</b></p>
                    <p>證照內容:{{ license.content }}</p>
                </div>
            </div>
            <hr>
        </div>
        <div class="card-body">
            <h3>經歷</h3>
            <div class="my-4" *ngFor="let job of profile.experiences.items">
                <h4>
                    {{ job.companyName }} 
                    <small class="text-muted">{{ job.position }}</small>
                </h4>
                <p>就職日:{{ job.startDate }}</p>
                <p>離職日:{{ job.endDate }}</p>
            </div>
        </div>
    </div>
</div>

成果展示

在輸入框輸入履歷位址並送出即可取得資訊:
https://ithelp.ithome.com.tw/upload/images/20190928/20119338SXnkMCRaTp.png


今日小結

終於~~~~完成了簡單的 DApp 應用 - 區塊鏈履歷!不過這只是很簡單的小專題來讓各位親自體驗製作 DApp ,所以在很多方面沒有特別做處理,像是防呆,我的想法是展示在現代前端技術下如何搭配 web3.js 來實作 DApp ,所以這方面就比較不是此篇的重點,請大家鞭小力一點/images/emoticon/emoticon16.gif
另外,本次的 DApp 與合約程式碼都會放在Github上面給各位參考!最後最後,明天還會再發一篇番外篇與總結,敬請期待!


上一篇
[區塊練起來-智能合約與DApp開發] DAY 29 - 實戰DApp!區塊鏈履歷應用(3)
下一篇
[區塊練起來-智能合約與DApp開發] DAY 31 - 閉幕式
系列文
區塊練起來-智能合約與DApp開發31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
tytom2003
iT邦新手 5 級 ‧ 2021-03-22 11:11:55

forkJoin(countReq).pipe(
switchMap(res => {
this.profile.setCounts(res);
return this.profile.setEducations();
}),

它告訴我"this.profile.setCounts(res);"的res有錯誤,
請你有沒有出現以下錯誤?
Argument of type 'unknown[]' is not assignable to parameter of type 'string[]'.
Type 'unknown' is not assignable to type 'string'.

Thank you very much.

HAO iT邦研究生 3 級 ‧ 2021-03-22 14:55:22 檢舉

你好,基本上這個 res 應該會是 string[] 型別,你可以將其指定為該型別,如:

switchMap((res: string[]) => {
    this.profile.setCounts(res);
    return this.profile.setEducations();
})

我要留言

立即登入留言