File

src/app/layout/picture-manager/picture-upload/picture-upload.component.ts

Implements

OnInit OnDestroy

Metadata

selector app-picture-upload
styleUrls ./picture-upload.component.less
templateUrl ./picture-upload.component.html

Index

Properties
Methods

Constructor

constructor(fb: FormBuilder, http: HttpClient, authService: AuthService, cookie: CookieService, cache: CacheService, router: Router, shareService: ShareService)
Parameters :
Name Type Optional
fb FormBuilder No
http HttpClient No
authService AuthService No
cookie CookieService No
cache CacheService No
router Router No
shareService ShareService No

Methods

cropperError
cropperError(ev: boolean)
Parameters :
Name Type Optional
ev boolean No
Returns : void
cropperReady
cropperReady(ev: boolean)
Parameters :
Name Type Optional
ev boolean No
Returns : void
cropperSelectAll
cropperSelectAll()
Returns : void
fileChange
fileChange(event: InputEvent)
Parameters :
Name Type Optional
event InputEvent No
Returns : void
goManager
goManager()
Returns : void
handleTags
handleTags(tag: string)
Parameters :
Name Type Optional
tag string No
Returns : void
imgSelectChange
imgSelectChange()
Returns : void
ngOnDestroy
ngOnDestroy()
Returns : void
ngOnInit
ngOnInit()
Returns : void
oneMore
oneMore()
Returns : void
showIntro
showIntro()
Returns : void
submitForm
submitForm()
Returns : void

Properties

aspectRatio
Type : number
Default value : 1.618
cropper
Type : Cropper
Default value : null
cropperLoading
Default value : false
file
Type : ElementRef<HTMLInputElement>
Decorators :
@ViewChild('file')
imageUrl
Type : string
Default value : ''
imgSelect
Type : string
Default value : 'imgUpload'
loadedImage
Type : HTMLImageElement
Default value : null
mode
Type : string
Default value : 'common'
pictureCroper
Type : PictureCroperComponent
Decorators :
@ViewChild('pictureCroper')
resizeToHeight
Type : number
Default value : 600
resizeToWidth
Type : number
Default value : 900
resizeToWidth2
Type : number
Default value : 900
rsPath
Type : string
Default value : ''
step
Type : number
Default value : 0
stopJitter
Default value : false
uploadApi
Default value : `${environment.host}/picture-manager/upload`
uploaded
Default value : false
uploadForm
Type : FormGroup
uploading
Default value : false
import { HttpClient } from '@angular/common/http';
import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { CookieService } from 'ngx-cookie-service';
import { throwError } from 'rxjs';
import { catchError, debounceTime } from 'rxjs/operators';
import { AuthService } from 'src/app/auth/auth.service';
import { CacheService } from 'src/app/share/cache.service';
import { ShareService } from 'src/app/share/share.service';
import { environment } from 'src/environments/environment';
import { PictureCroperComponent } from '../picture-croper/picture-croper.component';
import { Picture } from '../picture-manager';

@Component({
  selector: 'app-picture-upload',
  templateUrl: './picture-upload.component.html',
  styleUrls: ['./picture-upload.component.less']
})
export class PictureUploadComponent implements OnInit, OnDestroy {

  rsPath = '';
  imgSelect = 'imgUpload';
  cropperLoading = false;
  uploading = false;
  uploaded = false;
  step = 0;
  loadedImage: HTMLImageElement = null;
  aspectRatio = 1.618;
  resizeToWidth = 900;
  resizeToWidth2 = 900;
  resizeToHeight = 600;
  mode = 'common';
  cropper: Cropper = null;
  stopJitter = false; // 阻止表单提交的时候,cropbox比例抖动。

  uploadApi = `${environment.host}/picture-manager/upload`;
  imageUrl = ''; // 'https://fengyuanchen.github.io/cropperjs/images/picture.jpg';

  @ViewChild('file')
  file: ElementRef<HTMLInputElement>;

  @ViewChild('pictureCroper')
  pictureCroper: PictureCroperComponent;

  // 上传图片
  uploadForm!: FormGroup;

  constructor(
    private fb: FormBuilder,
    private http: HttpClient,
    private authService: AuthService,
    private cookie: CookieService,
    private cache: CacheService,
    private router: Router,
    private shareService: ShareService
  ) {
    // 保存上传图片
    this.uploadForm = this.fb.group({
      mode: ['common', [Validators.required]],
      site: [this.cookie.get('site'), [Validators.required]],
      aspectRatio: ['1.618/1', [Validators.required]],
      resizeToWidth: ['900', [Validators.required]],
      resizeToWidth2: ['900', [Validators.required]],
      resizeToHeight: ['600', [Validators.required]],
      keywords: [null, [Validators.required]],
    });

    // 变更模式
    this.uploadForm.get('mode').valueChanges.subscribe((value: string) => {
      if (this.stopJitter) { return; }
      this.mode = value;
      if (value === 'advance') {
        this.aspectRatio = this.resizeToWidth2 / this.resizeToHeight;
      } else {
        const aspectRatio = this.uploadForm.get('aspectRatio').value;
        if (aspectRatio === '0') {
          this.aspectRatio = NaN;
        } else {
          const na = aspectRatio.split('/');
          const n1 = Number.parseFloat(na[0]);
          const n2 = Number.parseFloat(na[1]);
          this.aspectRatio = n1 / n2;
        }
      }
      if (this.cropper) { this.cropper.setAspectRatio(this.aspectRatio); }
      // console.log('mode: ', this.mode, this.resizeToWidth, this.resizeToHeight);
    });

    // 图片比例
    this.uploadForm.get('aspectRatio').valueChanges.subscribe((value: string) => {
      if (this.stopJitter) { return; }
      if (this.mode === 'common') {
        if (value === '0') {
          this.aspectRatio = NaN;
        } else {
          const na = value.split('/');
          const n1 = Number.parseFloat(na[0]);
          const n2 = Number.parseFloat(na[1]);
          this.aspectRatio = n1 / n2;
        }
        if (this.cropper) { this.cropper.setAspectRatio(this.aspectRatio); }
      }
      // console.log('aspectRatio: ', this.aspectRatio);
    });

    // 图片宽度1
    this.uploadForm.get('resizeToWidth').valueChanges.subscribe((value: string) => {
      this.resizeToWidth = Number.parseInt(value, 10);
      // console.log('resizeToWidth: ', this.resizeToWidth);
    });

    // 图片宽度2
    this.uploadForm.get('resizeToWidth2').valueChanges.pipe(
      debounceTime(500)
    ).subscribe((value: string) => {
      if (this.stopJitter) { return; }
      this.resizeToWidth2 = Number.parseInt(value, 10);
      if (this.mode === 'advance') {
        this.aspectRatio = this.resizeToWidth2 / this.resizeToHeight;
        if (this.cropper) { this.cropper.setAspectRatio(this.aspectRatio); }
      }
      // console.log('resizeToWidth2: ', this.resizeToWidth2);
    });

    // 图片高度
    this.uploadForm.get('resizeToHeight').valueChanges.pipe(
      debounceTime(500)
    ).subscribe((value: string) => {
      if (this.stopJitter) { return; }
      this.resizeToHeight = Number.parseInt(value, 10);
      if (this.mode === 'advance') {
        this.aspectRatio = this.resizeToWidth2 / this.resizeToHeight;
        if (this.cropper) { this.cropper.setAspectRatio(this.aspectRatio); }
      }
      // console.log('resizeToHeight: ', this.resizeToHeight);
    });

    // 图片站点
    this.uploadForm.get('site').valueChanges.subscribe((value: string) => {
      this.cookie.set('site', value, new Date('2088-12-12'), undefined, undefined, undefined, 'Lax');
    });
  }

  fileChange(event: InputEvent) {
    this.step = 0;
    this.loadedImage = null;
    if (this.file.nativeElement.value) {
      this.step = 1;
      const files = this.file.nativeElement.files;
      // 文件是否存在
      if (files[0]) {
        if (window.FileReader) {
          this.cropperLoading = true;
          const fileReader = new FileReader();
          // 监听文件框
          fileReader.addEventListener('load', () => {
            this.imageUrl = fileReader.result as string;
          }, false);
          // 读取文件框的blob
          fileReader.readAsDataURL(files[0]);
        } else {
          alert('请使用最新版的 Google Chrome 浏览器。');
        }
      }
    }
  }

  imgSelectChange() {
    this.imageUrl = '';
  }

  cropperReady(ev: boolean) {
    if (ev) {
      this.cropper = this.pictureCroper.cropper;
      this.loadedImage = this.pictureCroper.imageElement;
      this.step = 1;
    }
  }

  cropperError(ev: boolean) {
    if (ev) {
      this.cropper = null;
      this.loadedImage = null;
      this.step = 0;
    }
  }

  cropperSelectAll() {
    this.uploadForm.patchValue({ aspectRatio: '0' });
    this.cropper?.setCropBoxData({
      left: 0,
      top: 0,
      width: this.loadedImage.width,
      height: this.loadedImage.height
    });
  }

  submitForm() {
    this.stopJitter = true;
    for (const key in this.uploadForm.controls) {
      if (this.uploadForm.controls.hasOwnProperty(key)) {
        this.uploadForm.controls[key].markAsDirty();
        this.uploadForm.controls[key].updateValueAndValidity();
        // console.log(`${key} => ${this.uploadForm.controls[key].value}`);
      }
    }
    const inputOrigin = this.imageUrl.length > 200 ? '' : this.imageUrl;
    const cachePicture = this.cache.get<Picture>('outsidePicture');
    if (this.uploadForm.valid && this.imageUrl) {
      this.step = 2;
      this.uploading = true;
      const modeWidth = this.mode === 'common' ? this.resizeToWidth : this.resizeToWidth2;
      const modeBase64 = this.cropper.getCroppedCanvas({
        imageSmoothingEnabled: false,
        imageSmoothingQuality: 'high',
      }).toDataURL('image/jpeg');
      // const modeHeight = modeWidth / this.aspectRatio;
      this.http.post(this.uploadApi, {
        base64: modeBase64,
        width: modeWidth < this.cropper.getData().width ? modeWidth : this.cropper.getData().width,
        // height: modeHeight < this.cropper.getData().height ? modeHeight : this.cropper.getData().height,
        user: this.authService.user.name || 'system',
        keywords: this.uploadForm.value.keywords || '',
        origin: inputOrigin || cachePicture?.picture.path || null,
        outside: cachePicture?.outside,
        site: this.uploadForm.value.site || 'all'
      }).pipe(
        catchError(() => {
          this.uploading = false;
          this.stopJitter = false;
          alert('图片上传失败,请重新试2-3次,如再不行,请联系lmr。');
          return throwError('Something bad happened; please try again later.');
        })
      ).subscribe((res: any) => {
        this.uploading = false;
        this.uploaded = true;
        this.rsPath = res.url;
        this.step = 3;
        this.stopJitter = false;
      });
    } else {
      // 延迟550ms执行,因为advance模式有输入防抖动500ms
      setTimeout(() => {
        this.stopJitter = false;
      }, 550);
    }
  }

  oneMore() {
    this.step = 1;
    // this.imageUrl = '';
    this.uploaded = false;
    // this.loadedImage = null;
    // this.file.nativeElement.value = '';
    // if (this.cropper) {
    //  this.cropper.destroy();
    //  this.cropper = null;
    // }
  }

  handleTags(tag: string) {
    this.uploadForm.patchValue({ keywords: tag });
  }

  goManager() {
    // this.cache.unset('cachePictures');
    // this.cache.unset('cachePage');
    // this.cache.unset('cacheSearchForm');
    this.router.navigateByUrl('/picture-manager/picture-storehouse');
  }

  showIntro() {
    this.shareService.showIntro().start();
  }

  ngOnInit(): void {

    // 是否存在待编辑的图片url
    const cachePicture = this.cache.get<Picture>('outsidePicture');
    if (cachePicture) {
      this.cropperLoading = true;
      this.imgSelect = 'imgSrc';
      this.imageUrl = cachePicture.outside ?
        cachePicture.picture.path : cachePicture.url;
    }

    // 页面引导
    if (!this.cookie.get('intro-picture-upload')) {
      setTimeout(() => {
        this.showIntro();
        this.cookie.set('intro-picture-upload', 'yes', new Date('2088-12-12'), undefined, undefined, undefined, 'Lax');
      }, 1500);
    }

  }

  ngOnDestroy() {
    if (this.cropper) {
      this.cropper.destroy();
      this.cropper = undefined;
    }
  }

}
<nz-space class="sop">
  <a *nzSpaceItem href="https://haina.yuque.com/docs/share/bf7f06f5-3204-4bbe-b69c-2de23883e0d3?# 《图片上传》"
    target="_blank" nz-button nzType="primary" nzSize="small" data-title="图片上传 Tips" data-intro="一些提升效率的小Tips。"
    data-step="99">
    <i nz-icon nzType="yuque" nzTheme="outline"></i>Tips
  </a>
  <button *nzSpaceItem nz-button nzType="primary" (click)="showIntro()" nzSize="small" data-title="图片上传 SOP"
    data-intro="最后:忘了怎么用?点这里再看一次。" data-step="100">
    <i nz-icon nzType="bulb" nzTheme="outline"></i>SOP
  </button>
</nz-space>
<nz-steps [nzCurrent]="step">
  <nz-step nzTitle="下方选择图片"></nz-step>
  <nz-step nzTitle="裁剪图片"> </nz-step>
  <nz-step nzTitle="填写属性,上传图片"></nz-step>
</nz-steps>
<nz-divider nzDashed [nzText]="text">
  <ng-template #text><i nz-icon nzType="picture" nzTheme="outline"></i></ng-template>
</nz-divider>
<div nz-row [nzGutter]="24">
  <div nz-col nzSpan="12">
    <nz-input-group [nzAddOnBefore]="addOnBeforeTemplate" class="img-from" data-title="图片上传SOP"
      data-intro="第1步:选择图片上传方式,使用本地上传或者图片地址。" data-step="1">
      <input type="text" nz-input [(ngModel)]="imageUrl" *ngIf="imgSelect === 'imgSrc'" />
      <input type="file" accept=".jpg,.webp,.png,.jpeg,.bmp,.gif" nz-input (change)="fileChange($event)"
        *ngIf="imgSelect === 'imgUpload'" #file />
    </nz-input-group>
    <ng-template #addOnBeforeTemplate>
      <nz-select [(ngModel)]="imgSelect" (ngModelChange)="imgSelectChange()">
        <nz-option nzLabel="本地图片" nzValue="imgUpload"></nz-option>
        <nz-option nzLabel="图片地址" nzValue="imgSrc"></nz-option>
      </nz-select>
    </ng-template>
    <div nz-row nzJustify="space-between">
      <div nz-col>
        <nz-tag [nzColor]="'blue'" *ngIf="loadedImage">
          原图尺寸:
          {{loadedImage.width}} x {{loadedImage.height}}
        </nz-tag>
      </div>
      <div nz-col>
        <nz-tag [nzColor]="'magenta'" class=" mr0" *ngIf="cropper">
          裁剪尺寸:
          {{cropper.getData(true).width}} x {{cropper.getData(true).height}}
        </nz-tag>
      </div>
    </div>
    <app-picture-croper #pictureCroper [imageUrl]="imageUrl" [aspectRatio]="aspectRatio" [loading]="cropperLoading"
      (cropperReady)="cropperReady($event)" (cropperError)="cropperError($event)" data-title="图片上传 SOP"
      data-intro="第6步:设置好右侧属性后,裁剪图片。" data-step="6"></app-picture-croper>
    <nz-divider></nz-divider>
    <button nz-button nzType="primary" *ngIf="cropper" (click)="cropperSelectAll()" nz-tooltip
      nzTooltipTitle="无视比例,选择图片所有区域,等效于不裁剪直接上传图片。">
      <i nz-icon nzType="expand" nzTheme="outline"></i>
      选择图片全部区域
    </button>
  </div>
  <div nz-col nzSpan="12">
    <form nz-form [formGroup]="uploadForm" (ngSubmit)="submitForm()">
      <nz-form-item>
        <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired nzFor="mode">裁剪模式</nz-form-label>
        <nz-radio-group formControlName="mode" id="mode" nzButtonStyle="solid">
          <label nz-radio-button nzValue="common">常规模式</label>
          <label nz-radio-button nzValue="advance">固定宽高</label>
        </nz-radio-group>
      </nz-form-item>
      <nz-form-item data-title="图片上传 SOP" data-intro="第2步:填写图片比例。" data-step="2" *ngIf="mode==='advance'">
        <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired nzFor="resizeToWidth2">图片宽度</nz-form-label>
        <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="图片宽度是必填项!" nzExtra="裁剪宽度小于最大宽度时,使用裁剪宽度。">
          <nz-input-group nzAddOnAfter="px">
            <nz-input-number formControlName="resizeToWidth2" id="resizeToWidth2" [nzMin]="100" [nzMax]="2560"
              [nzStep]="1">
            </nz-input-number>
          </nz-input-group>
        </nz-form-control>
      </nz-form-item>
      <nz-form-item data-title="图片上传 SOP" data-intro="第3步:填写图片高度。" data-step="3" *ngIf="mode==='advance'">
        <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired nzFor="resizeToHeight">图片高度</nz-form-label>
        <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="图片高度是必填项!" nzExtra="裁剪高度小于最大高度时,使用裁剪高度。">
          <nz-input-group nzAddOnAfter="px">
            <nz-input-number formControlName="resizeToHeight" id="resizeToHeight" [nzMin]="100" [nzMax]="2560"
              [nzStep]="1">
            </nz-input-number>
          </nz-input-group>
        </nz-form-control>
      </nz-form-item>
      <nz-form-item data-title="图片上传 SOP" data-intro="第2步:选择图片比例。" data-step="2" *ngIf="mode==='common'">
        <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired nzFor="aspectRatio">图片比例</nz-form-label>
        <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="图片比例是必填项!" nzExtra="图片的宽高比,注意使用规则。">
          <nz-select formControlName="aspectRatio" id="aspectRatio">
            <nz-option nzLabel="0:不限制" nzValue="0"></nz-option>
            <nz-option nzLabel="4/3:常规尺寸,两端效果不错,推荐" nzValue="4/3"></nz-option>
            <nz-option nzLabel="1.618/1:黄金分割比例,两端效果不错,推荐" nzValue="1.618/1"></nz-option>
            <nz-option nzLabel="3/2:单反相机默认比例,两端效果不错,推荐" nzValue="3/2"></nz-option>
            <nz-option nzLabel="1/1:正方形,用于小图标、二维码" nzValue="1/1"></nz-option>
            <nz-option nzLabel="1920/520:用于PC端的Banner大图" nzValue="1920/520"></nz-option>
          </nz-select>
        </nz-form-control>
      </nz-form-item>
      <nz-form-item data-title="图片上传 SOP" data-intro="第3步:选择图片最大宽度。" data-step="3" *ngIf="mode==='common'">
        <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired nzFor="resizeToWidth">最大宽度</nz-form-label>
        <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="图片宽度是必填项!" nzExtra="裁剪宽度小于最大宽度时,使用裁剪宽度。">
          <nz-select formControlName="resizeToWidth" id="resizeToWidth">
            <nz-option nzLabel="0:不限制" nzValue="10241024"></nz-option>
            <nz-option nzLabel="200:信息图片(SS)" nzValue="200"></nz-option>
            <nz-option nzLabel="450:信息图片(S)" nzValue="450"></nz-option>
            <nz-option nzLabel="600:信息图片(XS)" nzValue="600"></nz-option>
            <nz-option nzLabel="900:信息图片(M),推荐" nzValue="900"></nz-option>
            <nz-option nzLabel="1200:信息图片(L)" nzValue="1200"></nz-option>
            <nz-option nzLabel="1920:Banner大图(LL)" nzValue="1920"></nz-option>
          </nz-select>
        </nz-form-control>
      </nz-form-item>
      <nz-form-item data-title="图片上传 SOP" data-intro="第4步:选择图片发布的站点。" data-step="4">
        <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired nzFor="site">图片站点</nz-form-label>
        <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="图片站点是必填项!" nzExtra="选择在哪个站点上面显示。">
          <nz-select formControlName="site" id="site">
            <nz-option nzLabel="日语站" nzValue="jp"></nz-option>
            <nz-option nzLabel="西语站" nzValue="vac"></nz-option>
            <nz-option nzLabel="法语站" nzValue="vc"></nz-option>
            <nz-option nzLabel="德语站" nzValue="gm"></nz-option>
            <nz-option nzLabel="俄语站" nzValue="ru"></nz-option>
            <nz-option nzLabel="意语站" nzValue="it"></nz-option>
          </nz-select>
        </nz-form-control>
      </nz-form-item>
      <nz-form-item data-title="图片上传 SOP" data-intro="第5步:填写图片关键字。" data-step="5">
        <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired nzFor="keywords">关键字</nz-form-label>
        <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="关键字是必填项,推荐中文关键词,方便日后搜索图片。" nzExtra="请输入一或多个关键词,推荐中文关键词。">
          <textarea formControlName="keywords" nz-input rows="3"
            placeholder="推荐中文关键词,使用 “#” 隔开。&#13;&#10;例如:西藏#桂林#雷峰塔#白娘子。" hidden></textarea>
          <app-picture-tag-input (changeEvent)="handleTags($event)"></app-picture-tag-input>
        </nz-form-control>
      </nz-form-item>
      <nz-form-item nz-row data-title="图片上传 SOP" data-intro="第7步:保存图片,成功后下方可以点击复制图片地址。" data-step="7">
        <nz-form-control [nzSpan]="14" [nzOffset]="6">
          <button nz-button nzType="primary" [nzLoading]="uploading" *ngIf="!uploaded">
            保存图片
          </button>
          <div *ngIf="uploaded">
            <div class="show-picture">
              上传成功,图片地址:
              <p nz-typography nzCopyable [nzContent]="rsPath" [nzCopyTooltips]="['复制图片地址', '已复制']"></p>
              点击上方图标👆,复制图片地址。
            </div>
            <nz-divider></nz-divider>
            <nz-space>
              <a *nzSpaceItem nz-button nzType="link" (click)="oneMore();">再上传一张</a>
              <a *nzSpaceItem nz-button nzType="link" (click)="goManager()">跳转到图片管理</a>
            </nz-space>
          </div>
        </nz-form-control>
      </nz-form-item>
    </form>
  </div>
</div>

./picture-upload.component.less

[nz-form] {
  max-width: 600px;
  margin: 0 auto;
}

.img-from {
  margin-bottom: 12px;
}

.mr0 {
  margin-right: 0;
}

.show-picture {
  background-color: linen;
  border-radius: 6px;
  border: 1px solid #ddd;
  padding: 12px;
  word-break: break-all;
}

.sop {
  position: absolute;
  right: 50px;
  top: 80px;
}

nz-input-number {
  width: 100%;
}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""