前端拖拽实现商城首页设计,使用material drag and drop cdk

material drag and drop cdk

这个组件动态真嘟很丝滑~

  • 难点:是组件库拖拽时,不期望组件库的组件移动。通过插入一个假数据到menu实现页面组件不真实移动。
  • drop之后或者初始化完数据之后,需要手动触发视图检测,避免页面卡顿。
  •   this.changeDetectorRef.detectChanges();
    
  drop(event: any) {
    this.loading = true;
    if (event.previousContainer !== event.container) {
    	//copy item to target list ,为了实现menu数据drop前不消失,不能直接使用copyItemInArray方法
      // Clone the item that was dropped.
      const clone:IMenu = cloneDeep(event.previousContainer.data[event.previousIndex]);
      // Add the clone to the new array.
      event.container.data.splice(event.currentIndex, 0, clone);
     }else {
      //目标数组 数据排序
      if (event.previousIndex == this.selectedComponentIndex) this.selectedComponentIndex = event.currentIndex;
              moveItemInArray(
                event.container.data,
                event.previousIndex,
                event.currentIndex
              );
              
    }
     if (event.previousContainer.data) {
        this.menu = this.menu.filter((f) => !f.temp);
        //drop后过滤掉假数据
      }
    }
	 exited(event: any) {
	    const currentIdx = event.container.data.findIndex(
	      (f) => f.id === event.item.data.id
	    );
	    this.menu.splice(currentIdx + 1, 0, {
	      ...event.item.data,
	      temp: true,
	    });
	  }

部分drop 逻辑

export class PageDesignComponent{
}
  menu: Array<IMenu> = [
    { type: 'carousel',label:'轮播图' ,id: 1 },
    { type: 'productGroup',label:'商品组', id: 2 ,limit:4},
    // { type: 'voucherGroup',label:'优惠券组', id: 3 },
    { type: 'defaultVoucher',label:'优惠券', id: 3  },
    { type: 'popularProduct',label:'热销商品', id: 4 ,limit:4},
    { type: 'newProduct',label:'新品推荐', id: 5,limit:4 },
    // { type: 'groupon',label:'团购商品', id: 5 ,limit:2}
  ];

 drop(event: any) {
    this.loading = true;
    if (event.previousContainer !== event.container) {
      // Clone the item that was dropped.
      const clone:IMenu = cloneDeep(event.previousContainer.data[event.previousIndex]);
      // Add the clone to the new array.
      event.container.data.splice(event.currentIndex, 0, clone);
      this.pageConfig[event.currentIndex] = { data: [{ id: null, url: '', img: null }, { id: null, url: '', img: null }], type:  PageComponentType.CAROUSEL, ...clone };
      if (this.pageConfig[event.currentIndex].type == PageComponentType.CAROUSEL) {
      } else if (this.pageConfig[event.currentIndex].type == PageComponentType.PRODUCT_GROUP ) {
        this.pageConfig[event.currentIndex] = {
          name: clone.label,
          type:  PageComponentType.PRODUCT_GROUP ,
          composition:ArticleCompositionType.COLUMN,
          data: [{ id: null, name: '此处显示商品名称', url: '', img: null, price: 99 },
          { id: null, name: '此处显示商品名称', url: '', img: null, price: 99 },
          { id: null, name: '此处显示商品名称', url: '', img: null, price: 99 },
          { id: null, name: '此处显示商品名称', url: '', img: null, price: 99 }],
          ...clone
        };
      }else if(this.pageConfig[event.currentIndex].type == PageComponentType.POPULAR_PRODUCT|| this.pageConfig[event.currentIndex].type == PageComponentType.NEW_PRODUCT){
        this.pageConfig[event.currentIndex] = {
          name: clone.label,
          type: this.pageConfig[event.currentIndex].type ,
          composition:ArticleCompositionType.COLUMN,
          data:[],
          ...clone
        };
        ( new  ProductGroupDataInitInitializer(this.newProductPage,this.popularProductPage,this.groupProductPage))
       .initComponentData(this.pageConfig[event.currentIndex]);
        // this.initArticleConfig(this.pageConfig[event.].type,event.currecurrentIndexntIndex,clone);
      }else if (this.pageConfig[event.currentIndex].type ==  PageComponentType.VOUCHE_RGROUP) {
        this.pageConfig[event.currentIndex] = {
          name:clone.label,
          type:  PageComponentType.VOUCHE_RGROUP ,
          data: [{
            id: 301073,
            name: "折扣券 折",
            typeId: 100,
            offerAmt: 20,
            minPurchaseAmt: 100,
            senarios: 1,
            totalQty: 32,
            userTakeTotalQty: 1,
            validDay: 10
          },
          {
            id: 301073,
            name: "折扣券 折",
            typeId: 100,
            offerAmt: 20,
            minPurchaseAmt: 100,
            senarios: 1,
            totalQty: 32,
            userTakeTotalQty: 1,
            validDay: 10
          },
          {
            id: 301073,
            name: "折扣券 折",
            typeId: 100,
            offerAmt: 20,
            minPurchaseAmt: 100,
            senarios: 1,
            totalQty: 32,
            userTakeTotalQty: 1,
            validDay: 10
          }], ...clone
        }
      } else if (this.pageConfig[event.currentIndex].type ==  PageComponentType.DEFAULT_VOUCHER) {
        const config = {
          name:clone.label,
          type:  PageComponentType.DEFAULT_VOUCHER ,
          data:[...this.vouchers], ...clone
        }
        this.pageConfig[event.currentIndex] = config;
      }
    } else {
      if (event.previousIndex == this.selectedComponentIndex) this.selectedComponentIndex = event.currentIndex;
              moveItemInArray(
                event.container.data,
                event.previousIndex,
                event.currentIndex
              );
    }
    this.loading = false;
    this.selectedComponentEvent(event.currentIndex)
    setTimeout(() => {
      this.changeDetectorRef.detectChanges();
      if (event.previousContainer.data) {
        this.menu = this.menu.filter((f) => !f.temp);
      }
    });
  }
  exited(event: any) {
    const currentIdx = event.container.data.findIndex(
      (f) => f.id === event.item.data.id
    );
    this.menu.splice(currentIdx + 1, 0, {
      ...event.item.data,
      temp: true,
    });
  }

  entered() {
    this.menu = this.menu.filter((f) => !f.temp);
  }

在这里插入图片描述

html 使用ng-template 实现多种组件样式

<div class="page-design d-flex flex-row gap-2 p-3">
  <div class="p-3 border">
    <h3 class="px-1  border-start border-primary border-4">组件库</h3>
    <div class=" menu-list" cdkDropList #menuList="cdkDropList" [cdkDropListData]="menu" cdkDropListSortingDisabled
      [cdkDropListConnectedTo]="[tableList]" (cdkDropListDropped)="drop($event)" (cdkDropListExited)="exited($event)"
      (cdkDropListEntered)="entered()">
      <div>
        <div class="example-box d-flex jutsfy-content-center align-items-center" *ngFor="let item of menu" cdkDrag [cdkDragData]="item">
          <div class=" example-custom-placeholder d-flex flex-row justify-content-center gap-2 " style="  border: dotted 3px #999;" *cdkDragPlaceholder>
            <i *ngIf="item.type== 'carousel'" class="bi bi-images fs-4 align-self-center"></i>
            <i *ngIf="item.type== 'productGroup'" class="bi bi-grid-fill fs-4 align-self-center"></i>
            <i *ngIf="item.type== 'defaultVoucher'" class="bi  bi-ticket-perforated-fill fs-4 align-self-center"></i>
            <i *ngIf="item.type== 'popularProduct'" class="bi bi-bag-heart-fill fs-4 align-self-center"></i>
            <i *ngIf="item.type== 'newProduct'" class="bi  bi-star-fill fs-4 align-self-center"></i>
            <div class=" align-self-center">{{ item.label }}</div>
          </div>
          <div  class="d-flex flex-column gap-2  ">
            <i *ngIf="item.type== 'carousel'" class="bi bi-images fs-4 align-self-center"></i>
            <i *ngIf="item.type== 'productGroup'" class="bi bi-grid-fill fs-4 align-self-center"></i>
            <i *ngIf="item.type== 'defaultVoucher'" class="bi  bi-ticket-perforated-fill fs-4 align-self-center"></i>
            <i *ngIf="item.type== 'popularProduct'" class="bi bi-bag-heart-fill fs-4 align-self-center"></i>
            <i *ngIf="item.type== 'newProduct'" class="bi  bi-star-fill fs-4 align-self-center"></i>
            <a>{{ item.label }}</a>
          </div>
        </div>
        <div class="p-2 border-top flex-fill d-flex flex-row jutsfy-content-center ">
          <div class="btn btn-primary " (click)="save()">保存</div>
        </div>
      </div>
    </div>
  </div>
  <div class="page-center d-flex flex-column mx-3" >
     <nz-spin nzTip="加载中..." [nzSpinning]="loading">
      <div class="page-center d-flex flex-column flex-fill" cdkDropList #tableList="cdkDropList" [cdkDropListData]="pageConfig"(cdkDropListDropped)="drop($event)">
        <app-header></app-header>
        <div class="component-list d-flex flex-column  " style="overflow-y: auto;"> 
            <div  class="page-item-box  d-flex flex-column  p-2"  [class.selectedComponentItem]="idx == selectedComponentIndex"  ng-mouseover="itemMouseover()"    (click)="selectedComponentEvent(idx)"  *ngFor="let item of pageConfig; let idx = index" cdkDrag>
              <div class="page-item-box example-custom-placeholder d-flex flex-row justify-content-center gap-2" *cdkDragPlaceholder>
                <i *ngIf="item.type== 'carousel'" class="bi bi-images fs-4 align-self-center"></i>
                <i *ngIf="item.type== 'productGroup'" class="bi bi-grid-fill fs-4 align-self-center"></i>
                <i *ngIf="item.type== 'defaultVoucher'" class="bi  bi-ticket-perforated-fill fs-4 align-self-center"></i>
                <i *ngIf="item.type== 'popularProduct'" class="bi bi-bag-heart-fill fs-4 align-self-center"></i>
                <i *ngIf="item.type== 'newProduct'" class="bi  bi-star-fill fs-4 align-self-center"></i>
                <div class=" align-self-center">{{ item.label }}</div>
              </div>
              <ng-container *ngIf="item.type == 'carousel'">
                <ng-container *ngTemplateOutlet="carousel;context:{ $implicit: item?.data }"></ng-container>
              </ng-container>
              <ng-container *ngIf="item.type == 'productGroup'">
                <!-- <div class="d-flex flex-column  flex-fill "> -->
                  <div class="d-flex flex-row justify-content-between p-1">
                    <div class="d-flex ">{{item?.name }}</div>
                    <div class="d-flex">查看更多</div>
                  </div>
                  <div class="row gx-2 gy-2">
                    <ng-container *ngFor="let data of item?.data;let i = index">
                      <ng-container *ngIf="i < item.limit">
                        <ng-container *ngIf="item.composition == 'row' ">
                          <ng-container *ngTemplateOutlet="productCardRowTpl;context:{ $implicit: data ,index:i}"></ng-container>
                        </ng-container>
                        <ng-container *ngIf="item.composition == 'col' ">
                          <ng-container *ngTemplateOutlet="productCardColumnTpl;context:{ $implicit: data ,index:i}"></ng-container>
                        </ng-container>
                      </ng-container>
                    </ng-container>
                  </div>
                <!-- </div> -->
              </ng-container>
              <ng-container *ngIf="item.type == 'voucherGroup'">
                <ng-container *ngIf="item?.data?.length>0" >
                  <div>
                    {{item.name}}
                  </div>
                  <ng-container *ngTemplateOutlet="voucherGroup;context:{ $implicit: item.data}"></ng-container>
                </ng-container>
              </ng-container>
              <ng-container *ngIf="item.type == 'defaultVoucher'">
                <ng-container *ngIf="item?.data?.length>0" >
                  <div>
                    {{item.name}}
                  </div>
                  <ng-container *ngTemplateOutlet="voucherGroup;context:{ $implicit: item.data}"></ng-container>
                </ng-container>
              </ng-container>
              <ng-container *ngIf="item.type == 'newProduct'">
                <ng-container *ngTemplateOutlet="newProduct;context:{ $implicit: item}"></ng-container>
            </ng-container>
              <ng-container *ngIf="item.type == 'popularProduct'">
                  <ng-container *ngTemplateOutlet="popularProduct;context:{ $implicit: item}"></ng-container>
              </ng-container>
              <ng-template #listitem>
                <p>{{ idx }}--{{ item.type }}</p>
                <div class="desc">
                  <span>menuID:{{ item.id }}</span>
                  <!-- <span>price:{{ item.price | currency: 'CNY':'symbol':'4.2-2' }}</span> -->
                </div>
              </ng-template>
              <div *ngIf="idx == selectedComponentIndex" class="d-flex  flex-row gap-2 component-action-box">
                <div class="bg-primary px-1">
                  <p class="text-white" (click)="copyComponent($event)">复制</p>
                </div>
                <div class="bg-secondary px-1">
                  <p class="text-white" (click)="deleteComponent($event)">删除</p>
                </div>
              </div>
            </div>
        </div>
        
      </div>
     </nz-spin>
    
  </div>
  <div class="setting p-2 d-flex flex-column flex-fill border" >
    <h3 class="px-1 border-start border-primary border-4">{{selectedComponent?.label}}</h3>
   
    <div *ngIf="selectedComponentIndex != null" class="p-2 d-flex  flex-column gap-1 p-2 bg-white">
      <ng-container *ngIf="selectedComponent.type == 'carousel'">
        <ng-container *ngFor="let item of pageConfig[selectedComponentIndex]?.data;let i = index">
          <ng-container *ngTemplateOutlet="carouselPictureSetting;context:{ $implicit: item ,index:i}"></ng-container>
         </ng-container>
         <div class="p-1 bg-white "><div (click)="addCarouselPicture()" class="border setting-item p-1 d-flex flex-row justify-content-start align-items-center"><i class="bi bi-plus-lg fs-5"></i>添加图片</div></div>

      </ng-container>
      <ng-container *ngIf="selectedComponent.type == 'productGroup'">
        <ng-container *ngTemplateOutlet="prodcutSettingTpl;"></ng-container>
        <div  class="d-flex  flex-row gap-2 flex-wrap  p-2">
          <div style="position: relative;"  *ngFor="let item of pageConfig[selectedComponentIndex]?.data;let i = index" >
            <img style="width: 100px;height: 100px; object-fit:cover;" (error)="setDefaultPic($event)" src="{{getImgUrl(item.img)}}" />
            <div style="position: absolute;top: -1px;right:-1px;"><i (click)="deleteComponentDataByIndex(i)" class="text-light-emphasis bi bi-x-circle-fill fs-6"></i></div>
          </div>
          <!-- <ng-container *ngTemplateOutlet="carouselPictureSetting;context:{ $implicit: item ,index:i}"></ng-container> -->
         </div>
         <div class="p-1 bg-white "><div (click)="selectProduct()" class="border setting-item p-1 d-flex flex-row justify-content-start align-items-center"><i class="bi bi-plus-lg fs-5"></i>选择商品</div></div>
      </ng-container>
      <ng-container *ngIf="selectedComponent.type == 'voucherGroup'">
        <ng-container *ngTemplateOutlet="prodcutSettingTpl;"></ng-container>
        <ng-container *ngTemplateOutlet="voucherGroupSetting;context:{ $implicit: selectedComponent.data}"></ng-container>
        <div class="p-1 bg-white ">
          <div (click)="addVoucher()"
            class="border setting-item p-1 d-flex flex-row justify-content-start align-items-center"><i
              class="bi bi-plus-lg fs-5"></i>选择优惠券</div>
        </div>
      </ng-container>
      <ng-container *ngIf="selectedComponent.type == 'defaultVoucher'">
        <ng-container *ngTemplateOutlet="prodcutSettingTpl;"></ng-container>
        <ng-container *ngTemplateOutlet="voucherGroupSetting;context:{ $implicit: selectedComponent.data}"></ng-container>
        <!-- <div class="p-1 bg-white ">
          <div (click)="addVoucher()"
            class="border setting-item p-1 d-flex flex-row justify-content-start align-items-center"><i
              class="bi bi-plus-lg fs-5"></i>选择优惠券</div>
        </div> -->
      </ng-container>
      <ng-container *ngIf="selectedComponent.type == 'popularProduct'">
        <ng-container *ngTemplateOutlet="prodcutSettingTpl;"></ng-container>
        <!-- <div class="d-flex  flex-row gap-2 "><div class="align-self-center">名称:</div><div><input  nz-input [(ngModel)]="selectedComponent.name" /></div> </div> -->
      </ng-container>
      <ng-container *ngIf="selectedComponent.type == 'newProduct'">
        <!-- <div class="d-flex  flex-row gap-2 "><div class="align-self-center">名称:</div><div><input  nz-input [(ngModel)]="selectedComponent.name" /></div> </div> -->
        <ng-container *ngTemplateOutlet="prodcutSettingTpl;"></ng-container>
      </ng-container>
      <ng-container *ngIf="selectedComponent.type == 'groupon'">
        <!-- <div class="d-flex  flex-row gap-2 "><div class="align-self-center">名称:</div><div><input  nz-input [(ngModel)]="selectedComponent.name" /></div> </div> -->
        <ng-container *ngTemplateOutlet="prodcutSettingTpl;"></ng-container>
      </ng-container>
    </div>
  </div>
</div>

初始化数据时为了用了设计模式的策略模式,减少了代码冗余

//策略类
class ProductGroupDataInitInitializer {
  dataPages = {};
  constructor(
    private newProductPage: PageArticleMallEntity,
    private popularProductPage: PageArticleMallEntity,
    private groupProductPage: PageArticleMallEntity,
  ) {
    this.dataPages = {
      newProduct: this.newProductPage?.records,
      popularProduct: this.popularProductPage?.records,
      groupon: this.groupProductPage?.records
    };
  }
  initComponentData(component: ComoponentInterface) {
    const data = this.dataPages[component.type];
    if (data && data.length > 0) {
      let i = 0;
      for (let article of data) {
        if (article != null) {
          component.data[i++] = { articleId: article.articleId, name: article.name, url: '', img: article?.img, price: article.price }
        }
      }
    } else {
      this.setDefaultTemplate(component);
    }

  }
  private setDefaultTemplate(component: ComoponentInterface) {
    component.data = [{ id: null, name: '此处显示商品名称', url: '', img: null, price: 99 },
    { id: null, name: '此处显示商品名称', url: '', img: null, price: 99 },
    { id: null, name: '此处显示商品名称', url: '', img: null, price: 99 },
    { id: null, name: '此处显示商品名称', url: '', img: null, price: 99 }]
  }

}

//使用
 initData(channel:ChannelDetailEntity) {
    const timeZone=Intl.DateTimeFormat().resolvedOptions().timeZone ;
    forkJoin([
      this.mallArticleService.getFeaturedProduct(channel.channelId, "new", 1, 8).pipe(catchError((err) =>{ console.error(err); return of(null)})),
      this.mallArticleService.getFeaturedProduct(channel.channelId, "popular", 1, 8).pipe(catchError((err) =>{ console.error(err); return of(null)})),
      this.voucherService.getVouchersAvailableToTake(-1,format(new Date(),'yyyyMMdd'),timeZone).pipe(catchError((err) =>{ console.error(err); return of(null)}))
    ]).subscribe(
      ([newProductPage, popularProductPage, vouchers]: [PageArticleMallEntity, PageArticleMallEntity, VoucherSchemeEntity[]])=>{
        if (newProductPage != null)
          this.newProductPage = newProductPage;
        if (popularProductPage != null)
          this.popularProductPage = popularProductPage;
        if (vouchers != null)
          this.vouchers = vouchers;

        const id = Number.parseInt(this.route.snapshot.paramMap.get('id'));
        if (id > 0) {
          this.mallPageService.getPage(id).subscribe(
            res => {
              if (res != null) {
                this.webPageConfig = res;
                this.pageConfig = JSON.parse(res.content);
                for(let component of this.pageConfig){
                  if(component.type == PageComponentType.DEFAULT_VOUCHER){
                    component.data = [...this.vouchers];
                  }else if(component.type == PageComponentType.NEW_PRODUCT){
                  //初始化策略类
                    ( new  ProductGroupDataInitInitializer(this.newProductPage,this.popularProductPage,this.groupProductPage))
                    .initComponentData(component);
                  }
                }
              }
            }
          );
          }
      }
    );
   
  }  

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值