Angular组件(二) 分割面板ShrinkSplitter

Angular组件(二) 分割面板ShrinkSplitter

前言

在Angular组件(一) 分割面板ShrinkSplitter文章中我们实现了Splitter组件,后来在业务场景中发现在开关右侧容器和底部容器时,使用起来不方便,ngModel绑定的值始终是左侧容器和顶部容器的大小,然而有时我们关注的是右侧容器和底部容器的大小,让左侧自适应。于是修改组件代码,让ngmodel绑定的容器大小和tlColsedMode关联,举例: tlColsedMode = “right”,ngModel绑定的值就是右侧容器的大小。

组件Splitter

module.ts

import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { TlShrinkSplitterComponent } from "./shrink-splitter.component";
import{NzToolTipModule} from "ng-zorro-antd/tooltip"const COMMENT = [TlShrinkSplitterComponent];@NgModule({declarations: [...COMMENT],exports: [...COMMENT],imports: [CommonModule,NzToolTipModule,]
})
export class TlShrinkSplitterModule {}

component.ts

import { AfterContentInit, AfterViewInit, ChangeDetectorRef, Component, ContentChildren, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, QueryList, TemplateRef, ViewChild } from "@angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
import { TlTemplateDirective } from "topdsm-lib/common"
import { isFalsy } from "topdsm-lib/core/util";
import { off, on } from "./util";@Component({selector: "tl-shrink-splitter",templateUrl: "./shrink-splitter.component.html",providers: [{provide: NG_VALUE_ACCESSOR,useExisting: forwardRef(() => TlShrinkSplitterComponent),multi: true}],host: {class: "tl-shrink-splitter",'[class.expand]': 'tlExpand','[class.contract]': '!tlExpand','[class.dragable]': 'tlDragable','[class.contract-left]': 'tlColsedMode === "left"','[class.contract-right]': 'tlColsedMode === "right"','[class.contract-top]': 'tlColsedMode === "top"','[class.contract-bottom]': 'tlColsedMode === "bottom"','[style.z-index]': 'tlZIndex',}
})
export class TlShrinkSplitterComponent implements OnInit, AfterContentInit, AfterViewInit, ControlValueAccessor {prefix = "tl-shrink-splitter"offset = 0oldOffset: number | string = 0isMoving = falseinitOffset = 0_value: number | string = 0.5isOpen = trueviewRender = false@Input()tlZIndex = 10/** 是否展示收起icon */@Input()tlShowExpandIcon = true/** 收起容器模式,上下左右哪一个容器应用收起展开的状态 */@Input()tlColsedMode: "left" | "right" | "top" | "bottom" = "left"@Input()tlMin = "40px"@Input()tlMax = "40px"@Input()tlExpandTooltipContent = ""@Input()tlContractTooltipContent = ""/** 是否可拖拽调整大小 */@Input()tlDragable = trueget value() {return this._value}set value(val: number | string) {if(!this.viewRender && !this.tlExpand){           this.expandValueCache = valval = 0}this._value = valthis.onChange(val)this.computeOffset()setTimeout(() => {this.viewRender = true}, 0);}expandValueCache: string | number = 0/** 展开状态 */get tlExpand() {return this.isOpen;}@Input()set tlExpand(val: boolean) {if (val !== this.isOpen) {this.isOpen = val;this.tlExpandChange.emit(val);this.changeExpand(val)}}/** 容器展开状态切换 */changeExpand(status: boolean) {if (!status) {// 收起this.expandValueCache = this.valuethis.value = 0} else {// 展开this.value = this.expandValueCachethis.expandValueCache = 0}}/** 展开收缩切换事件 */@Output() readonly tlExpandChange = new EventEmitter<boolean>();@Output() readonly onMoveStart = new EventEmitter();@Output() readonly onMoving = new EventEmitter<MouseEvent>();@Output() readonly onMoveEnd = new EventEmitter();expandChange(e: MouseEvent) {e.stopPropagation();e.preventDefault()this.tlExpand = !this.isOpen}@ContentChildren(TlTemplateDirective)templates?: QueryList<TlTemplateDirective>leftTemplate?: TemplateRef<void> | null = nullrightTemplate?: TemplateRef<void> | null = nulltopTemplate?: TemplateRef<void> | null = nullbottomTemplate?: TemplateRef<void> | null = null@ViewChild('outerWrapper')outerWrapper: ElementRef;get isHorizontal() {return this.tlColsedMode === "left" || this.tlColsedMode === "right"}get computedMin() {return this.getComputedThresholdValue('tlMin');}get computedMax() {return this.getComputedThresholdValue('tlMax');}get anotherOffset() {return 100 - this.offset;}get valueIsPx() {return typeof this.value === 'string';}get offsetSize() {return this.isHorizontal ? 'offsetWidth' : 'offsetHeight';}get paneClasses() {let classes = {}classes[`${this.prefix}-pane`] = trueclasses[`${this.prefix}-pane-transition`] = this.viewRenderclasses[`${this.prefix}-pane-moving`] = this.isMovingreturn classes}/** 展开收起触发器icon */get triggrrClass() {let classes = {}if (this.tlColsedMode === "left" && this.isOpen) {classes["icon-caret-left"] = true} else if (this.tlColsedMode === "left" && !this.isOpen) {classes["icon-caret-right"] = true} else if (this.tlColsedMode === "right" && this.isOpen) {classes["icon-caret-right"] = true} else if (this.tlColsedMode === "right" && !this.isOpen) {classes["icon-caret-left"] = true} else if (this.tlColsedMode === "top" && this.isOpen) {classes["icon-caret-left"] = true} else if (this.tlColsedMode === "top" && !this.isOpen) {classes["icon-caret-right"] = true} else if (this.tlColsedMode === "bottom" && this.isOpen) {classes["icon-caret-right"] = true} else if (this.tlColsedMode === "bottom" && !this.isOpen) {classes["icon-caret-left"] = true}return classes}get tooltipPosition() {let position = "right"if (this.tlColsedMode === "right" && !this.isOpen) {position = "left"}return position}get tooltipContent() {let tooltip = ""if (this.tlColsedMode === "left" && this.isOpen) {tooltip = isFalsy(this.tlExpandTooltipContent) ? "收起左侧内容" : this.tlExpandTooltipContent} else if (this.tlColsedMode === "left" && !this.isOpen) {tooltip = isFalsy(this.tlContractTooltipContent) ? "展开左侧内容" : this.tlContractTooltipContent} else if (this.tlColsedMode === "right" && this.isOpen) {tooltip = isFalsy(this.tlExpandTooltipContent) ? "收起右侧内容" : this.tlExpandTooltipContent} else if (this.tlColsedMode === "right" && !this.isOpen) {tooltip = isFalsy(this.tlContractTooltipContent) ? "展开右侧内容" : this.tlContractTooltipContent} else if (this.tlColsedMode === "top" && this.isOpen) {tooltip = isFalsy(this.tlExpandTooltipContent) ? "收起顶部内容" : this.tlExpandTooltipContent} else if (this.tlColsedMode === "top" && !this.isOpen) {tooltip = isFalsy(this.tlContractTooltipContent) ? "展开顶部内容" : this.tlContractTooltipContent} else if (this.tlColsedMode === "bottom" && this.isOpen) {tooltip = isFalsy(this.tlExpandTooltipContent) ? "收起底部内容" : this.tlExpandTooltipContent} else if (this.tlColsedMode === "bottom" && !this.isOpen) {tooltip = isFalsy(this.tlContractTooltipContent) ? "展开底部内容" : this.tlContractTooltipContent}return tooltip}px2percent(numerator: string | number, denominator: string | number) {return parseFloat(numerator + "") / parseFloat(denominator + "");}computeOffset() {if(this.tlColsedMode === "left" || this.tlColsedMode === "top"){           this.offset = (this.valueIsPx ? this.px2percent(this.value as string, this.outerWrapper.nativeElement[this.offsetSize]) : this.value) as number * 10000 / 100}else{this.offset = (this.valueIsPx ? 1 - this.px2percent(this.value as string, this.outerWrapper.nativeElement[this.offsetSize]) : 1- (this.value as number)) as number * 10000 / 100}  }getComputedThresholdValue(type) {let size = this.outerWrapper.nativeElement[this.offsetSize];if (this.valueIsPx) return typeof this[type] === 'string' ? this[type] : size * this[type];else return typeof this[type] === 'string' ? this.px2percent(this[type], size) : this[type];}getMin(value1, value2) {if (this.valueIsPx) return `${Math.min(parseFloat(value1), parseFloat(value2))}px`;else return Math.min(value1, value2);}getMax(value1, value2) {if (this.valueIsPx) return `${Math.max(parseFloat(value1), parseFloat(value2))}px`;else return Math.max(value1, value2);}getAnotherOffset(value) {let res: string | number = 0;if (this.valueIsPx) res = `${this.outerWrapper.nativeElement[this.offsetSize] - parseFloat(value)}px`;else res = 1 - value;return res;}handleMove = (e) => {let pageOffset = this.isHorizontal ? e.pageX : e.pageY;let offset = pageOffset - this.initOffset;let outerWidth = this.outerWrapper.nativeElement[this.offsetSize];let value: string | number = ""if (this.valueIsPx) {           if(this.tlColsedMode === "left" || this.tlColsedMode === "top"){value = `${parseFloat(this.oldOffset as string) + offset}px`}else{value = `${parseFloat(this.oldOffset as string) - offset}px`}} else {if(this.tlColsedMode === "left" || this.tlColsedMode === "top"){value = this.px2percent(outerWidth * (this.oldOffset as number) + offset, outerWidth)}else{value = this.px2percent(outerWidth * (this.oldOffset as number) - offset, outerWidth)}         }let anotherValue = this.getAnotherOffset(value);if (parseFloat(value + "") <= parseFloat(this.computedMin + "")) value = this.getMax(value, this.computedMin);if (parseFloat(anotherValue + "") <= parseFloat(this.computedMax)) value = this.getAnotherOffset(this.getMax(anotherValue, this.computedMax));e.atMin = this.value === this.computedMin;e.atMax = this.valueIsPx ? this.getAnotherOffset(this.value) === this.computedMax : (this.getAnotherOffset(this.value) as number).toFixed(5) === this.computedMax.toFixed(5);this.value = valuethis.onMoving.emit(e)}handleUp = (e) => {this.isMoving = false;off(document, 'mousemove', this.handleMove);off(document, 'mouseup', this.handleUp);this.onMoveEnd.emit()}onTriggerMouseDown(e) {if(!this.tlDragable){return}this.initOffset = this.isHorizontal ? e.pageX : e.pageY;this.oldOffset = this.value;this.isMoving = true;on(document, 'mousemove', this.handleMove);on(document, 'mouseup', this.handleUp);this.onMoveStart.emit()}constructor(private cdr: ChangeDetectorRef) { }ngOnInit(): void {console.log("ngOnInit");}ngAfterViewInit(): void {console.log("ngAfterViewInit");this.computeOffset()}ngAfterContentInit() {this.templates?.forEach((item) => {switch (item.getType()) {case 'left':this.leftTemplate = item.template;break;case 'right':this.rightTemplate = item.template;break;case 'top':this.topTemplate = item.template;break;case 'bottom':this.bottomTemplate = item.template;break;default:this.leftTemplate = item.template;break;}});}// 输入框数据变化时onChange: (value: any) => void = () => null;onTouched: () => void = () => null;writeValue(val: number | string): void {if (val !== this.value) {this.value = valthis.computeOffset();this.cdr.markForCheck();}}// UI界面值发生更改,调用注册的回调函数registerOnChange(fn: any): void {this.onChange = fn;}// 在blur(等失效事件),调用注册的回调函数registerOnTouched(fn: any): void {this.onTouched = fn;}// 设置禁用状态setDisabledState?(isDisabled: boolean): void {}
}

TlTemplateDirective指令实现

import { Directive, Input, TemplateRef, ViewContainerRef } from "@angular/core";
import { NzSafeAny } from "topdsm-lib/core/types";@Directive({selector: '[tlTemplate]'
})
export class TlTemplateDirective {@Input('tlTemplate')name: string = "default"// @Input()// type: string = ""constructor(private viewContainer: ViewContainerRef, public template: TemplateRef<NzSafeAny>) {//this.template = templateRef;}ngOnInit(): void {this.viewContainer.createEmbeddedView(this.template)}getType() {return this.name;}
}

事件绑定、解绑

export const on = (function() {if (document.addEventListener) {return function(element, event, handler) {if (element && event && handler) {element.addEventListener(event, handler, false);}};} else {return function(element, event, handler) {if (element && event && handler) {element.attachEvent('on' + event, handler);}};}
})();export const off = (function() {if (document.removeEventListener) {return function(element, event, handler) {if (element && event) {element.removeEventListener(event, handler, false);}};} else {return function(element, event, handler) {if (element && event) {element.detachEvent('on' + event, handler);}};}
})();

component.html

<div [ngClass]="prefix + '-wrapper'" #outerWrapper><div [ngClass]="prefix + '-horizontal'" *ngIf="isHorizontal; else verticalSlot"><div class="left-pane" [ngStyle]="{right: anotherOffset + '%'}" [ngClass]="paneClasses"><ng-container *ngTemplateOutlet="leftTemplate"></ng-container></div><div [ngClass]="prefix + '-trigger-con'" [ngStyle]="{left: offset + '%'}" (mousedown)="onTriggerMouseDown($event)"><div ngClass="tl-shrink-splitter-trigger tl-shrink-splitter-trigger-vertical" ><span class="tl-shrink-splitter-trigger-bar-con vertical" [ngClass]="triggrrClass" (mousedown)="expandChange($event)" nz-tooltip [nzTooltipTitle]="tooltipContent" [nzTooltipPlacement]="tooltipPosition" *ngIf="tlShowExpandIcon"></span></div></div><div class="right-pane" [ngStyle]="{left: offset + '%'}" [ngClass]="paneClasses"><ng-container *ngTemplateOutlet="rightTemplate"></ng-container></div></div><ng-template #verticalSlot><div [ngClass]="prefix + '-vertical'" ><div class="top-pane" [ngStyle]="{bottom: anotherOffset + '%'}" [ngClass]="paneClasses"><ng-container *ngTemplateOutlet="topTemplate"></ng-container></div><div [ngClass]="prefix + '-trigger-con'" [ngStyle]="{top: offset + '%'}" (mousedown)="onTriggerMouseDown($event)"><div ngClass="tl-shrink-splitter-trigger tl-shrink-splitter-trigger-horizontal" ><span class="tl-shrink-splitter-trigger-bar-con horizontal" [ngClass]="triggrrClass" (mousedown)="expandChange($event)" nz-tooltip [nzTooltipTitle]="tooltipContent" [nzTooltipPlacement]="tooltipPosition" *ngIf="tlShowExpandIcon"></span></div></div><div class="bottom-pane" [ngStyle]="{top: offset + '%'}" [ngClass]="paneClasses"><ng-container *ngTemplateOutlet="bottomTemplate"></ng-container></div></div></ng-template>
</div>

component.less

@split-prefix-cls: ~"tl-shrink-splitter";
@trigger-bar-background: rgba(23, 35, 61, 0.25);
@trigger-background: #f3f4f7;
@trigger-width: 8px;
@trigger-bar-width: 4px;
@trigger-bar-offset: (@trigger-width - @trigger-bar-width) / 2;
@trigger-bar-interval: 3px;
@trigger-bar-weight: 1px;
@trigger-bar-con-height: 24px;
@trigger-bar-con-width: 24px;
.tl-shrink-splitter{position: relative;height: 100%;width: 100%;
}
.tl-shrink-splitter-wrapper{position: relative;height: 100%;width: 100%;
}.@{split-prefix-cls}{background-color: #fff;border: 1px solid #dee2e6;&-pane{position: absolute;//transition: all .3s ease-in;padding: 8px;&.tl-shrink-splitter-pane-moving{transition: none;}&.left-pane, &.right-pane {top: 0;bottom: 0;}&.left-pane {left: 0;}&.right-pane {right: 0;padding-left: 16px;}&.top-pane, &.bottom-pane {left: 0;right: 0;}&.top-pane {top: 0;}&.bottom-pane {bottom: 0;padding-top: 16px;}&-moving{-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;}&-transition{transition: all .3s ease-in; }}&-trigger{border: 1px solid #dcdee2;&-con {position: absolute;transform: translate(-50%, -50%);z-index: 10;}&-bar-con {position: absolute;overflow: hidden;background-image: linear-gradient(90deg, #dcf3fc, #f8fdff);border: 1px solid #76b9d6;color: #76b9d6;&:hover{color: #000 !important;}&.vertical {top: 50%;left: -6px;width: @trigger-bar-con-width;height: @trigger-bar-con-height;//background-color: #fff;//border: 1px solid #ccc;border-radius: 50%;display: flex;align-items: center;justify-content: center;//color: #b2b2b2;font-size: 14px;cursor: pointer;}&.horizontal {left: 50%;top: -4px;width: @trigger-bar-con-height;height: @trigger-bar-con-width;//transform: translate(-50%, 0);background-color: #fff;border: 1px solid #ccc;border-radius: 50%;display: flex;align-items: center;justify-content: center;color: #b2b2b2;font-size: 14px;cursor: pointer;}}&-vertical {width: @trigger-width;height: 100%;background: @trigger-background;border-top: none;border-bottom: none;//cursor: col-resize;.@{split-prefix-cls}-trigger-bar {width: @trigger-bar-width;height: 1px;background: @trigger-bar-background;float: left;margin-top: @trigger-bar-interval;}}&-horizontal {height: @trigger-width;width: 100%;background: @trigger-background;border-left: none;border-right: none;//cursor: row-resize;.@{split-prefix-cls}-trigger-bar {height: @trigger-bar-width;width: 1px;background: @trigger-bar-background;float: left;margin-right: @trigger-bar-interval;}}}&-horizontal {.@{split-prefix-cls}-trigger-con {top: 50%;height: 100%;width: 0;}}&-vertical {.@{split-prefix-cls}-trigger-con {left: 50%;height: 0;width: 100%;}}
}.tl-shrink-splitter.dragable{.tl-shrink-splitter-trigger-vertical{cursor: col-resize;}.tl-shrink-splitter-trigger-horizontal{cursor: row-resize;}
}.tl-shrink-splitter.contract{.tl-shrink-splitter-trigger-vertical{width: 0;padding-left: 0;}.tl-shrink-splitter-trigger-horizontal{height: 0;padding-top: 0;}.tl-shrink-splitter-trigger{border: 0;}&.contract-left{.tl-shrink-splitter-pane.left-pane{width: 0;padding: 0;overflow: hidden;}.right-pane{padding-left: 8px;}}.tl-shrink-splitter-trigger-bar-con{&.vertical{left: -6px;}}&.contract-right{.tl-shrink-splitter-trigger-bar-con{&.vertical{left: -16px;}}}&.contract-top{.tl-shrink-splitter-pane.top-pane{overflow: hidden;height: 0;padding: 0;}.bottom-pane{padding-top: 8px;}.tl-shrink-splitter-trigger-bar-con.horizontal{transform: rotate(90deg);}}&.contract-bottom{.tl-shrink-splitter-trigger-bar-con{&.horizontal{top: -16px;}}.tl-shrink-splitter-pane.bottom-pane{overflow: hidden;height: 0;padding: 0;}.top-pane{padding-top: 8px;}.tl-shrink-splitter-trigger-bar-con.horizontal{transform: rotate(90deg);}}
}
.tl-shrink-splitter.expand{.tl-shrink-splitter-trigger-bar-con{&.vertical{left: -8px;}}&.contract-top{.tl-shrink-splitter-trigger-bar-con.horizontal{transform: rotate(90deg);}}&.contract-bottom{.tl-shrink-splitter-trigger-bar-con.horizontal{transform: rotate(90deg);}}
}

页面效果

左右容器和上下容器
image.png

demo

左侧容器

import { Component } from '@angular/core';@Component({selector: 'tl-demo-shrink-splitter-basic',template: `<button tButton type="button" label="切换伸缩状态" class="ui-plusWidth ui-button-primary" style="margin-right: 8px" (click)="expandChange()"></button><div class="split-box"><tl-shrink-splitter [(tlExpand)]="expand" [(ngModel)]="value" (onMoving)="triggerMoveHnadle($event)"><ng-template tlTemplate="left"><div>左侧区域自定义</div></ng-template><ng-template tlTemplate="right"><div>右侧区域自定义</div></ng-template></tl-shrink-splitter>  </div>`,styles: [`.split-box{height: 200px;display: flex;position: relative;overflow: hidden;}.split-right{margin-left: 10px;border: 1px solid #e3e3e3;flex:1;}`]
})
export class TlDemoShrinkSplitterBasicComponent {expand = truevalue = 0.3expandChange(){this.expand = !this.expand}triggerMoveHnadle(e){console.log(e);console.log(this.value);  }
}

image.png

image.png

右侧容器

import { Component } from '@angular/core';@Component({selector: 'tl-demo-shrink-splitter-right',template: `<button tButton type="button" label="切换伸缩状态" class="ui-plusWidth ui-button-primary" style="margin-right: 8px" (click)="expandChange()"></button><div class="split-box"><tl-shrink-splitter [(tlExpand)]="expand" [(ngModel)]="value" tlColsedMode="right"><ng-template tlTemplate="left"><div>左侧区域自定义</div></ng-template><ng-template tlTemplate="right"><div>右侧区域自定义</div></ng-template></tl-shrink-splitter>  </div>`,styles: [`.split-box{height: 300px;display: flex;position: relative;overflow: hidden;}.split-right{margin-left: 10px;border: 1px solid #e3e3e3;flex:1;}`]
})
export class TlDemoShrinkSplitterRightComponent {expand = truevalue = "200px"expandChange(){this.expand = !this.expand}
}

在这里插入图片描述

顶部容器

import { Component } from '@angular/core';@Component({selector: 'tl-demo-shrink-splitter-top',template: `<button tButton type="button" label="切换伸缩状态" class="ui-plusWidth ui-button-primary" style="margin-right: 8px" (click)="expandChange()"></button><div class="split-box"><tl-shrink-splitter [(tlExpand)]="expand" [(ngModel)]="value"  tlColsedMode="top"><ng-template tlTemplate="top"><div>顶部区域自定义</div></ng-template><ng-template tlTemplate="bottom"><div>底部区域自定义</div></ng-template></tl-shrink-splitter>  </div>`,styles: [`.split-box{height: 300px;display: flex;position: relative;overflow: hidden;}.split-right{margin-left: 10px;border: 1px solid #e3e3e3;flex:1;}`]
})
export class TlDemoShrinkSplitterTopComponent {expand = truevalue = "100px"expandChange(){this.expand = !this.expand}
}

在这里插入图片描述

底部容器

import { Component } from '@angular/core';@Component({selector: 'tl-demo-shrink-splitter-bottom',template: `<button tButton type="button" label="切换伸缩状态" class="ui-plusWidth ui-button-primary" style="margin-right: 8px" (click)="expandChange()"></button><div class="split-box"><tl-shrink-splitter [(tlExpand)]="expand" [(ngModel)]="value"  tlColsedMode="bottom"><ng-template tlTemplate="top"><div>顶部区域自定义</div></ng-template><ng-template tlTemplate="bottom"><div>底部区域自定义</div></ng-template></tl-shrink-splitter>  </div>`,styles: [`.split-box{height: 300px;display: flex;position: relative;overflow: hidden;}.split-right{margin-left: 10px;border: 1px solid #e3e3e3;flex:1;}`]
})
export class TlDemoShrinkSplitterBottomComponent {expand = truevalue = "100px"expandChange() {this.expand = !this.expand}
}

在这里插入图片描述

API

tl-shrink-splitter

参数说明类型默认值
[ngModel]面板位置,可以是 0~1 代表百分比,或具体数值的像素,可用 ngModel 双向绑定string | number0.5
[tlExpand]是否展开容器booleantrue
[tlDragable]是否可以拖拽拖拽容器大小booleantrue
[tlZIndex]组件z-indexnumber10
[tlShowExpandIcon]是否展示收起展开切换状态的按钮iconbooleantrue
[tlColsedMode]收起容器模式,上下左右哪一个容器应用收起展开的状态'left' | 'right' | 'top' | 'bottom'left
[tlMin]最小阈值,类型为number时,数值范围为0 ~ 1 表示宽度百分比string | number40px
[tlMax]最大阈值,类型为number时,数值范围为0 ~ 1 表示宽度百分比string| number40px
[tlExpandTooltipContent]展开状态时的tooltip
[tlContractTooltipContent]收起状态时的tooltip

事件

事件名参数描述
tlExpandChangeevt(Boolean)容器展开/收起 change
onMoveStart拖拽开始
onMovingevt(MouseEvent)拖拽中
onMoveEnd拖拽结束

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/657732.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Flutter App 生命周期观察监听

前言 本文主要讲解两种 Flutter生命周期观察监听 方式一&#xff1a;Flutter SDK 3.13 之前的方式&#xff0c;WidgetsBindingObserver&#xff1b; 方式二&#xff1a;Flutter SDK 3.13 开始的新方式&#xff0c;AppLifecycleListener&#xff1b; 测试平台&#xff1a;IO…

走进水稻种植教学基地可视化:科技与农业知识的完美结合

随着科技的不断发展&#xff0c;农业领域也在不断创新和进步。水稻种植教学基地可视化系统是一种基于现代信息技术手段的教学方式&#xff0c;通过虚拟现实、3D建模等技术&#xff0c;将水稻种植的全过程进行模拟和展示。这种教学方式打破了传统农业教学的局限性&#xff0c;使…

idea中yml文件没有提示解决办法

两步解决yml文件不显示提示&#xff0c;yaml文件显示提示问题 1、在插件中心&#xff0c;先下载下图两个插件 2、在Editor》File Types新增文件类型&#xff0c;文件名匹配规则需要将yaml和yml的都加上&#xff0c;加好之后&#xff0c;重启idea&#xff0c;即刻生效。

C++进阶--继承

概念 继承&#xff0c;允许一个类&#xff08;称为子类或派生类&#xff09;从另一个类&#xff08;称为父类或基类&#xff09;继承属性和方法。 继承的主要目的是实现代码的重用和构建类之间的层次关系。通过继承&#xff0c;子类可以获得父类的特性&#xff0c;包括数据成员…

嵌合抗体介绍-泰克生物

一&#xff0e;嵌合抗体简介 人-鼠嵌合抗体&#xff0c;即抗体的可变区来自鼠单克隆抗体&#xff0c;而恒定区则来自人的抗体。它是通过从杂交瘤细胞分离出功能性可变区基因&#xff0c;与人Ig恒定区基因连接,插入适当表达载体&#xff0c;转染宿主细胞表达产生。嵌合抗体既保留…

centOS+nodejs+mysql阿里云部署前后端个人网站

centOSnodejsmysql阿里云部署前后端个人网站 参考&#xff1a; 部署NodeExpressMySQL项目到阿里云轻量应用服务器 阿里云轻量应用服务器部署Node.jsReactMongoDB前后端分离项目 参考&#xff1a;在阿里云上部署nodejs服务 https 部署的原理就是你在本地测试的时候在地址栏&am…

给零基础朋友的编程课12 代码

给零基础朋友的编程课12 下 - 仿制品7 案例_哔哩哔哩_bilibili 源代码&#xff1a; // 色表 // 桃红 254,181,167 // 粉红 255,208,199void setup() {size(1000,750);background(254,181,167); }void draw() {// 绘制画框stroke(255,208,199);strokeWeight(28);noFill();rect…

Cmake编译Opencv3.3.1遇到有些文件无法下载的错误解决:

前言&#xff1a; 对于&#xff0c;opencv有些配置文件错误并未致命&#xff0c;所以&#xff0c;有错误也不影响后续的编译&#xff1a;但是&#xff0c;后引用如果要用&#xff0c;在回过头来还是要解决的。 问题表述&#xff1a; 比如&#xff0c;有些文件下载的错误&am…

Linux可视化管理

记得看目录哦&#xff01; 1. webmin2. webmin安装及配置2.1 把软件传到opt目录下的webmin目录2.2 解压rpm -ivh webmin-1.700-1.noarch.rpm2.3 重置webmin的root密码2.4 修改webmin的端口2.5 重启webmin2.6 放开6666端口的防火墙2.7 网址输入ip&#xff1a;端口号 3. webmin的…

自建DNS劫持服务器,纯内网劫持PS5,屏蔽更新,自动hen

背景&#xff1a;目前PS5首次折腾必须要连外网&#xff0c;还要改DNS&#xff0c;除非使用ESP8266/32&#xff0c; 本文的方法是完全不改DNS&#xff0c;不使用ESP8266,不连接外网的情况下自动折腾 能实现什么&#xff1a; 1.折腾全程不连接外网 2.完全自建hen服务器&#xff…

维护管理Harbor,docker容器的重启策略

维护管理Harbor 通过HarborWeb创建项目 在 Harbor 仓库中&#xff0c;任何镜像在被 push 到 regsitry 之前都必须有一个自己所属的项目。 单击“项目”&#xff0c;填写项目名称&#xff0c;项目级别若设置为"私有"&#xff0c;则不勾选。如果设置为公共仓库&#…

[C++]类和对象(上)

目录 一:面向过程与面向对象的区别 二:类的定义 三:类的访问限定符和封装 3.1访问限定符 3.2 封装 四:类的实例化 五:类对象模型 如何计算类的大小 类对象的存储方式 六:this指针 this指针的引出 this指针的特性 一:面向过程与面向对象的区别 面向过程 C语言是面…

[机器学习]TF-IDF算法

一.TF-IDF算法概述 什么是TF-IDF&#xff1f; 词频-逆文档频率&#xff08;Term Frequency-Inverse Document Frequency&#xff0c;TF-IDF&#xff09;是一种常用于文本处理的统计方法&#xff0c;可以评估一个单词在一份文档中的重要程度。简单来说就是可以用于文档关键词的提…

SQL注入:宽字节注入

SQL注入系列文章&#xff1a; 初识SQL注入-CSDN博客 SQL注入&#xff1a;联合查询的三个绕过技巧-CSDN博客 SQL注入&#xff1a;报错注入-CSDN博客 SQL注入&#xff1a;盲注-CSDN博客 SQL注入&#xff1a;二次注入-CSDN博客 ​SQL注入&#xff1a;order by注入-CSDN博客 …

wordcloud库和jieba库的使用

文章目录 wordcloud库的简单示范使用wordcloud库报错记录anaconda安装第三方jieba库jieba库的简单示范任务 1&#xff1a;三国演义中的常见词汇分布在“三国"这两个隶书字上&#xff0c;出现频率高的词字体大任务 2&#xff1a;三国演义中出现频率前十的人名。必须是以下这…

JZ15 二进制中1的个数(牛客)(C语言)

个人博客主页&#xff1a;https://blog.csdn.net/2301_79293429?typeblog 专栏&#xff1a;https://blog.csdn.net/2301_79293429/category_12545690.html 该题我为笨办法,与题解不同,如有疑问和见解,欢迎大家在评论区提出 题目链接: 二进制中1的个数_牛客题霸_牛客网 (now…

【日常总结】如何快速迁移Navicat中的全部连接设置到新安装的Navicat中?

一、场景 二、需求 三、解决方案 Stage 1&#xff1a;“文件”-->“导出连接”。 Stage 2&#xff1a;获取备份文件 connections.ncx Stage 3&#xff1a;导入connections.ncx 四、不足 一、场景 公司电脑换新&#xff0c;所有软件需要重装&#xff0c;包括navicat 1…

如何纯前端实现文件下载

业务场景 有一个下载文件的功能&#xff0c;不引入后端资源&#xff0c;纯前端应该如何实现&#xff1f; 解决方案 在vue2或者vue3项目中&#xff0c;可以把文件放在 public 文件夹下&#xff0c;然后使用a标签进行文件下载。 如&#xff1a;我要下载的文件是模版.xlsx 。首…

Django模型(五)

一、数据的条件查询 参考文档:QuerySet API 参考 | Django 文档 | Django 1.1、常用检索字段 字段检索,是在字段名后加 __ 双下划线,再加关键字,类似 SQL 语句中的 where 后面的部分, 如: 字段名__关键字 exact :判断是否等于value,一般不使用,而直接使用 =contai…

PL/SQL plsql Developer 14最新版注册码 (亲测可用)

plsql14 注册激活&#xff0c;亲测有效 product code: ke4tv8t5jtxz493kl8s2nn3t6xgngcmgf3 serial Number: 264452 password: xs374ca 激活成功