init from https://github.com/Hufe921/canvas-editor
This commit is contained in:
commit
977ba1c283
456 files changed
+52892
No files matched your search
@@ -0,0 +1,171 @@
|
||||
import { EditorComponent, EDITOR_COMPONENT } from '../../editor'
|
||||
import './dialog.css'
|
||||
|
||||
export interface IDialogData {
|
||||
type: string
|
||||
label?: string
|
||||
name: string
|
||||
value?: string
|
||||
options?: { label: string; value: string }[]
|
||||
placeholder?: string
|
||||
width?: number
|
||||
height?: number
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
export interface IDialogConfirm {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface IDialogOptions {
|
||||
onClose?: () => void
|
||||
onCancel?: () => void
|
||||
onConfirm?: (payload: IDialogConfirm[]) => void
|
||||
title: string
|
||||
data: IDialogData[]
|
||||
}
|
||||
|
||||
export class Dialog {
|
||||
private options: IDialogOptions
|
||||
private mask: HTMLDivElement | null
|
||||
private container: HTMLDivElement | null
|
||||
private inputList: (
|
||||
| HTMLInputElement
|
||||
| HTMLTextAreaElement
|
||||
| HTMLSelectElement
|
||||
)[]
|
||||
|
||||
constructor(options: IDialogOptions) {
|
||||
this.options = options
|
||||
this.mask = null
|
||||
this.container = null
|
||||
this.inputList = []
|
||||
this._render()
|
||||
}
|
||||
|
||||
private _render() {
|
||||
const { title, data, onClose, onCancel, onConfirm } = this.options
|
||||
// 渲染遮罩层
|
||||
const mask = document.createElement('div')
|
||||
mask.classList.add('dialog-mask')
|
||||
mask.setAttribute(EDITOR_COMPONENT, EditorComponent.COMPONENT)
|
||||
document.body.append(mask)
|
||||
// 渲染容器
|
||||
const container = document.createElement('div')
|
||||
container.classList.add('dialog-container')
|
||||
container.setAttribute(EDITOR_COMPONENT, EditorComponent.COMPONENT)
|
||||
// 弹窗
|
||||
const dialogContainer = document.createElement('div')
|
||||
dialogContainer.classList.add('dialog')
|
||||
container.append(dialogContainer)
|
||||
// 标题容器
|
||||
const titleContainer = document.createElement('div')
|
||||
titleContainer.classList.add('dialog-title')
|
||||
// 标题&关闭按钮
|
||||
const titleSpan = document.createElement('span')
|
||||
titleSpan.append(document.createTextNode(title))
|
||||
const titleClose = document.createElement('i')
|
||||
titleClose.onclick = () => {
|
||||
if (onClose) {
|
||||
onClose()
|
||||
}
|
||||
this._dispose()
|
||||
}
|
||||
titleContainer.append(titleSpan)
|
||||
titleContainer.append(titleClose)
|
||||
dialogContainer.append(titleContainer)
|
||||
// 选项容器
|
||||
const optionContainer = document.createElement('div')
|
||||
optionContainer.classList.add('dialog-option')
|
||||
// 选项
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const option = data[i]
|
||||
const optionItemContainer = document.createElement('div')
|
||||
optionItemContainer.classList.add('dialog-option__item')
|
||||
// 选项名称
|
||||
if (option.label) {
|
||||
const optionName = document.createElement('span')
|
||||
optionName.append(document.createTextNode(option.label))
|
||||
optionItemContainer.append(optionName)
|
||||
if (option.required) {
|
||||
optionName.classList.add('dialog-option__item--require')
|
||||
}
|
||||
}
|
||||
// 选项输入框
|
||||
let optionInput:
|
||||
| HTMLInputElement
|
||||
| HTMLTextAreaElement
|
||||
| HTMLSelectElement
|
||||
if (option.type === 'select') {
|
||||
optionInput = document.createElement('select')
|
||||
option.options?.forEach(item => {
|
||||
const optionItem = document.createElement('option')
|
||||
optionItem.value = item.value
|
||||
optionItem.label = item.label
|
||||
optionInput.append(optionItem)
|
||||
})
|
||||
} else if (option.type === 'textarea') {
|
||||
optionInput = document.createElement('textarea')
|
||||
} else {
|
||||
optionInput = document.createElement('input')
|
||||
optionInput.type = option.type
|
||||
}
|
||||
if (option.width) {
|
||||
optionInput.style.width = `${option.width}px`
|
||||
}
|
||||
if (option.height) {
|
||||
optionInput.style.height = `${option.height}px`
|
||||
}
|
||||
optionInput.name = option.name
|
||||
optionInput.value = option.value || ''
|
||||
if (!(optionInput instanceof HTMLSelectElement)) {
|
||||
optionInput.placeholder = option.placeholder || ''
|
||||
}
|
||||
optionItemContainer.append(optionInput)
|
||||
optionContainer.append(optionItemContainer)
|
||||
this.inputList.push(optionInput)
|
||||
}
|
||||
dialogContainer.append(optionContainer)
|
||||
// 按钮容器
|
||||
const menuContainer = document.createElement('div')
|
||||
menuContainer.classList.add('dialog-menu')
|
||||
// 取消按钮
|
||||
const cancelBtn = document.createElement('button')
|
||||
cancelBtn.classList.add('dialog-menu__cancel')
|
||||
cancelBtn.append(document.createTextNode('取消'))
|
||||
cancelBtn.type = 'button'
|
||||
cancelBtn.onclick = () => {
|
||||
if (onCancel) {
|
||||
onCancel()
|
||||
}
|
||||
this._dispose()
|
||||
}
|
||||
menuContainer.append(cancelBtn)
|
||||
// 确认按钮
|
||||
const confirmBtn = document.createElement('button')
|
||||
confirmBtn.append(document.createTextNode('确定'))
|
||||
confirmBtn.type = 'submit'
|
||||
confirmBtn.onclick = () => {
|
||||
if (onConfirm) {
|
||||
const payload = this.inputList.map<IDialogConfirm>(input => ({
|
||||
name: input.name,
|
||||
value: input.value
|
||||
}))
|
||||
onConfirm(payload)
|
||||
}
|
||||
this._dispose()
|
||||
}
|
||||
menuContainer.append(confirmBtn)
|
||||
dialogContainer.append(menuContainer)
|
||||
// 渲染
|
||||
document.body.append(container)
|
||||
this.container = container
|
||||
this.mask = mask
|
||||
}
|
||||
|
||||
private _dispose() {
|
||||
this.mask?.remove()
|
||||
this.container?.remove()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
.dialog-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: .5;
|
||||
background: #000000;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.dialog-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
z-index: 999;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
position: absolute;
|
||||
padding: 0 30px 30px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%);
|
||||
border: 1px solid #e2e6ed;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
position: relative;
|
||||
border-bottom: 1px solid #e2e6ed;
|
||||
margin-bottom: 30px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dialog-title i {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
background: url(../../assets/images/close.svg);
|
||||
}
|
||||
|
||||
.dialog-option__item {
|
||||
margin-bottom: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dialog-option__item span {
|
||||
margin-right: 12px;
|
||||
font-size: 14px;
|
||||
color: #3d4757;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dialog-option__item input,
|
||||
.dialog-option__item textarea,
|
||||
.dialog-option__item select {
|
||||
width: 276px;
|
||||
height: 30px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #d3d3d3;
|
||||
min-height: 30px;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
appearance: none;
|
||||
user-select: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.dialog-option__item input:focus,
|
||||
.dialog-option__item textarea:focus {
|
||||
border-color: #4991f2;
|
||||
}
|
||||
|
||||
.dialog-option__item--require::before {
|
||||
content: "*";
|
||||
color: #f56c6c;
|
||||
margin-right: 4px;
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
}
|
||||
|
||||
.dialog-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-menu button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border: 1px solid #e2e6ed;
|
||||
border-radius: 2px;
|
||||
background: #ffffff;
|
||||
line-height: 22px;
|
||||
padding: 0 16px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dialog-menu button:hover {
|
||||
background: rgba(25, 55, 88, .04);
|
||||
}
|
||||
|
||||
.dialog-menu__cancel {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.dialog-menu button[type='submit'] {
|
||||
color: #ffffff;
|
||||
background: #4991f2;
|
||||
border-color: #4991f2;
|
||||
}
|
||||
|
||||
.dialog-menu button[type='submit']:hover {
|
||||
background: #5b9cf3;
|
||||
border-color: #5b9cf3;
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
import { EditorComponent, EDITOR_COMPONENT } from '../../editor'
|
||||
import './signature.css'
|
||||
|
||||
export interface ISignatureResult {
|
||||
value: string
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface ISignatureOptions {
|
||||
width?: number
|
||||
height?: number
|
||||
onClose?: () => void
|
||||
onCancel?: () => void
|
||||
onConfirm?: (payload: ISignatureResult | null) => void
|
||||
}
|
||||
|
||||
export class Signature {
|
||||
private readonly MAX_RECORD_COUNT = 1000
|
||||
private readonly DEFAULT_WIDTH = 390
|
||||
private readonly DEFAULT_HEIGHT = 180
|
||||
private undoStack: Array<Function> = []
|
||||
private x = 0
|
||||
private y = 0
|
||||
private isDrawing = false
|
||||
private isDrawn = false
|
||||
private linePoints: [number, number][] = []
|
||||
private canvasWidth: number
|
||||
private canvasHeight: number
|
||||
private options: ISignatureOptions
|
||||
private mask: HTMLDivElement
|
||||
private container: HTMLDivElement
|
||||
private trashContainer: HTMLDivElement
|
||||
private undoContainer: HTMLDivElement
|
||||
private canvas: HTMLCanvasElement
|
||||
private ctx: CanvasRenderingContext2D
|
||||
private preTimeStamp: number
|
||||
private dpr: number
|
||||
|
||||
constructor(options: ISignatureOptions) {
|
||||
this.options = options
|
||||
this.preTimeStamp = 0
|
||||
this.dpr = window.devicePixelRatio
|
||||
this.canvasWidth = (options.width || this.DEFAULT_WIDTH) * this.dpr
|
||||
this.canvasHeight = (options.height || this.DEFAULT_HEIGHT) * this.dpr
|
||||
const { mask, container, trashContainer, undoContainer, canvas } =
|
||||
this._render()
|
||||
this.mask = mask
|
||||
this.container = container
|
||||
this.trashContainer = trashContainer
|
||||
this.undoContainer = undoContainer
|
||||
this.canvas = canvas
|
||||
this.ctx = <CanvasRenderingContext2D>canvas.getContext('2d')
|
||||
this.ctx.scale(this.dpr, this.dpr)
|
||||
this.ctx.lineCap = 'round'
|
||||
this._bindEvent()
|
||||
this._clearUndoFn()
|
||||
// this is necessary so that the screen does not move when moving - it is removed when closing the modal
|
||||
document.documentElement.classList.add('overflow-hidden')
|
||||
document.body.classList.add('overflow-hidden')
|
||||
this.container.classList.add('overflow-hidden')
|
||||
}
|
||||
|
||||
private _render() {
|
||||
const { onClose, onCancel, onConfirm } = this.options
|
||||
// 渲染遮罩层
|
||||
const mask = document.createElement('div')
|
||||
mask.classList.add('signature-mask')
|
||||
mask.setAttribute(EDITOR_COMPONENT, EditorComponent.COMPONENT)
|
||||
document.body.append(mask)
|
||||
// 渲染容器
|
||||
const container = document.createElement('div')
|
||||
container.classList.add('signature-container')
|
||||
container.setAttribute(EDITOR_COMPONENT, EditorComponent.COMPONENT)
|
||||
// 弹窗
|
||||
const signatureContainer = document.createElement('div')
|
||||
signatureContainer.classList.add('signature')
|
||||
container.append(signatureContainer)
|
||||
// 标题容器
|
||||
const titleContainer = document.createElement('div')
|
||||
titleContainer.classList.add('signature-title')
|
||||
// 标题&关闭按钮
|
||||
const titleSpan = document.createElement('span')
|
||||
titleSpan.append(document.createTextNode('插入签名'))
|
||||
const titleClose = document.createElement('i')
|
||||
titleClose.onclick = () => {
|
||||
if (onClose) {
|
||||
onClose()
|
||||
}
|
||||
this._dispose()
|
||||
}
|
||||
titleContainer.append(titleSpan)
|
||||
titleContainer.append(titleClose)
|
||||
signatureContainer.append(titleContainer)
|
||||
// 操作区
|
||||
const operationContainer = document.createElement('div')
|
||||
operationContainer.classList.add('signature-operation')
|
||||
// 撤销
|
||||
const undoContainer = document.createElement('div')
|
||||
undoContainer.classList.add('signature-operation__undo')
|
||||
const undoIcon = document.createElement('i')
|
||||
const undoLabel = document.createElement('span')
|
||||
undoLabel.innerText = '撤销'
|
||||
undoContainer.append(undoIcon)
|
||||
undoContainer.append(undoLabel)
|
||||
operationContainer.append(undoContainer)
|
||||
// 清空画布
|
||||
const trashContainer = document.createElement('div')
|
||||
trashContainer.classList.add('signature-operation__trash')
|
||||
const trashIcon = document.createElement('i')
|
||||
const trashLabel = document.createElement('span')
|
||||
trashLabel.innerText = '清空'
|
||||
trashContainer.append(trashIcon)
|
||||
trashContainer.append(trashLabel)
|
||||
operationContainer.append(trashContainer)
|
||||
signatureContainer.append(operationContainer)
|
||||
// 绘图区
|
||||
const canvasContainer = document.createElement('div')
|
||||
canvasContainer.classList.add('signature-canvas')
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = this.canvasWidth
|
||||
canvas.height = this.canvasHeight
|
||||
canvas.style.width = `${this.canvasWidth / this.dpr}px`
|
||||
canvas.style.height = `${this.canvasHeight / this.dpr}px`
|
||||
canvasContainer.append(canvas)
|
||||
signatureContainer.append(canvasContainer)
|
||||
// 按钮容器
|
||||
const menuContainer = document.createElement('div')
|
||||
menuContainer.classList.add('signature-menu')
|
||||
// 取消按钮
|
||||
const cancelBtn = document.createElement('button')
|
||||
cancelBtn.classList.add('signature-menu__cancel')
|
||||
cancelBtn.append(document.createTextNode('取消'))
|
||||
cancelBtn.type = 'button'
|
||||
cancelBtn.onclick = () => {
|
||||
if (onCancel) {
|
||||
onCancel()
|
||||
}
|
||||
this._dispose()
|
||||
}
|
||||
menuContainer.append(cancelBtn)
|
||||
// 确认按钮
|
||||
const confirmBtn = document.createElement('button')
|
||||
confirmBtn.append(document.createTextNode('确定'))
|
||||
confirmBtn.type = 'submit'
|
||||
confirmBtn.onclick = () => {
|
||||
if (onConfirm) {
|
||||
onConfirm(this._toData())
|
||||
}
|
||||
this._dispose()
|
||||
}
|
||||
menuContainer.append(confirmBtn)
|
||||
signatureContainer.append(menuContainer)
|
||||
// 渲染
|
||||
document.body.append(container)
|
||||
this.container = container
|
||||
this.mask = mask
|
||||
return {
|
||||
mask,
|
||||
canvas,
|
||||
container,
|
||||
trashContainer,
|
||||
undoContainer
|
||||
}
|
||||
}
|
||||
|
||||
private _bindEvent() {
|
||||
this.trashContainer.onclick = this._clearCanvas.bind(this)
|
||||
this.undoContainer.onclick = this._undo.bind(this)
|
||||
this.canvas.onmousedown = this._startDraw.bind(this)
|
||||
this.canvas.onmousemove = this._draw.bind(this)
|
||||
this.container.onmouseup = this._stopDraw.bind(this)
|
||||
this.container.ontouchmove = this.registerTouchmove.bind(this)
|
||||
this.container.ontouchstart = this.registerTouchstart.bind(this)
|
||||
this.container.ontouchend = this.registerTouchend.bind(this)
|
||||
}
|
||||
|
||||
private _undo() {
|
||||
if (this.undoStack.length > 1) {
|
||||
this.undoStack.pop()
|
||||
if (this.undoStack.length) {
|
||||
this.undoStack[this.undoStack.length - 1]()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _saveUndoFn(fn: Function) {
|
||||
this.undoStack.push(fn)
|
||||
while (this.undoStack.length > this.MAX_RECORD_COUNT) {
|
||||
this.undoStack.shift()
|
||||
}
|
||||
}
|
||||
|
||||
private _clearUndoFn() {
|
||||
const clearFn = () => {
|
||||
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
|
||||
}
|
||||
this.undoStack = [clearFn]
|
||||
}
|
||||
|
||||
private _clearCanvas() {
|
||||
this._clearUndoFn()
|
||||
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
|
||||
}
|
||||
|
||||
private _startDraw(evt: MouseEvent) {
|
||||
this.isDrawing = true
|
||||
this.x = evt.offsetX
|
||||
this.y = evt.offsetY
|
||||
this.ctx.lineWidth = 1
|
||||
}
|
||||
|
||||
private _draw(evt: MouseEvent) {
|
||||
if (!this.isDrawing) return
|
||||
// 计算鼠标移动速度
|
||||
const curTimestamp = performance.now()
|
||||
const distance = Math.sqrt(evt.movementX ** 2 + evt.movementY ** 2)
|
||||
const speed = distance / (curTimestamp - this.preTimeStamp)
|
||||
// 目标线宽:最小速度1,最大速度5,系数3
|
||||
const SPEED_FACTOR = 3
|
||||
const targetLineWidth = Math.min(5, Math.max(1, 5 - speed * SPEED_FACTOR))
|
||||
// 平滑过渡算法(20%的变化比例)调整线条粗细:系数0.2
|
||||
const SMOOTH_FACTOR = 0.2
|
||||
this.ctx.lineWidth =
|
||||
this.ctx.lineWidth * (1 - SMOOTH_FACTOR) + targetLineWidth * SMOOTH_FACTOR
|
||||
// 绘制
|
||||
const { offsetX, offsetY } = evt
|
||||
this.ctx.beginPath()
|
||||
this.ctx.moveTo(this.x, this.y)
|
||||
this.ctx.lineTo(offsetX, offsetY)
|
||||
this.ctx.stroke()
|
||||
this.x = offsetX
|
||||
this.y = offsetY
|
||||
this.linePoints.push([offsetX, offsetY])
|
||||
this.isDrawn = true
|
||||
// 缓存之前时间戳
|
||||
this.preTimeStamp = curTimestamp
|
||||
}
|
||||
|
||||
private _stopDraw() {
|
||||
this.isDrawing = false
|
||||
if (this.isDrawn) {
|
||||
const imageData = this.ctx.getImageData(
|
||||
0,
|
||||
0,
|
||||
this.canvasWidth,
|
||||
this.canvasHeight
|
||||
)
|
||||
const self = this
|
||||
this._saveUndoFn(function () {
|
||||
self.ctx.clearRect(0, 0, self.canvasWidth, self.canvasHeight)
|
||||
self.ctx.putImageData(imageData, 0, 0)
|
||||
})
|
||||
this.isDrawn = false
|
||||
}
|
||||
}
|
||||
|
||||
private _toData(): ISignatureResult | null {
|
||||
if (!this.linePoints.length) return null
|
||||
// 查找矩形四角坐标
|
||||
const startX = this.linePoints[0][0]
|
||||
const startY = this.linePoints[0][1]
|
||||
let minX = startX
|
||||
let minY = startY
|
||||
let maxX = startX
|
||||
let maxY = startY
|
||||
for (let p = 0; p < this.linePoints.length; p++) {
|
||||
const point = this.linePoints[p]
|
||||
if (minX > point[0]) {
|
||||
minX = point[0]
|
||||
}
|
||||
if (maxX < point[0]) {
|
||||
maxX = point[0]
|
||||
}
|
||||
if (minY > point[1]) {
|
||||
minY = point[1]
|
||||
}
|
||||
if (maxY < point[1]) {
|
||||
maxY = point[1]
|
||||
}
|
||||
}
|
||||
// 增加边框宽度
|
||||
const lineWidth = this.ctx.lineWidth
|
||||
minX = minX < lineWidth ? 0 : minX - lineWidth
|
||||
minY = minY < lineWidth ? 0 : minY - lineWidth
|
||||
maxX = maxX + lineWidth
|
||||
maxY = maxY + lineWidth
|
||||
const sw = maxX - minX
|
||||
const sh = maxY - minY
|
||||
// 裁剪图像
|
||||
const imageData = this.ctx.getImageData(
|
||||
minX * this.dpr,
|
||||
minY * this.dpr,
|
||||
sw * this.dpr,
|
||||
sh * this.dpr
|
||||
)
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.style.width = `${sw}px`
|
||||
canvas.style.height = `${sh}px`
|
||||
canvas.width = sw * this.dpr
|
||||
canvas.height = sh * this.dpr
|
||||
const ctx = <CanvasRenderingContext2D>canvas.getContext('2d')!
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
const value = canvas.toDataURL()
|
||||
return {
|
||||
value,
|
||||
width: sw,
|
||||
height: sh
|
||||
}
|
||||
}
|
||||
|
||||
private registerTouchmove(evt: TouchEvent) {
|
||||
this.registerTouchEvent(evt, 'mousemove')
|
||||
}
|
||||
|
||||
private registerTouchstart(evt: TouchEvent) {
|
||||
this.registerTouchEvent(evt, 'mousedown')
|
||||
}
|
||||
|
||||
private registerTouchend() {
|
||||
const me = new MouseEvent('mouseup', {})
|
||||
this.canvas.dispatchEvent(me)
|
||||
}
|
||||
|
||||
private registerTouchEvent(evt: TouchEvent, eventName: string) {
|
||||
const touch = evt.touches[0]
|
||||
const me = new MouseEvent(eventName, {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
})
|
||||
this.canvas.dispatchEvent(me)
|
||||
}
|
||||
|
||||
private _dispose() {
|
||||
this.mask.remove()
|
||||
this.container.remove()
|
||||
document.documentElement.classList.remove('overflow-hidden')
|
||||
document.body.classList.remove('overflow-hidden')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
.signature-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: .5;
|
||||
background: #000000;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.signature-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
z-index: 999;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.signature {
|
||||
position: absolute;
|
||||
padding: 0 30px 30px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%);
|
||||
border: 1px solid #e2e6ed;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.signature-title {
|
||||
position: relative;
|
||||
border-bottom: 1px solid #e2e6ed;
|
||||
margin-bottom: 15px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.signature-title i {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
background: url(../../assets/images/close.svg);
|
||||
}
|
||||
|
||||
.signature-operation>div {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #3d4757;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.signature-operation>div:hover {
|
||||
color: #6e7175;
|
||||
}
|
||||
|
||||
.signature-operation>div i {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.signature-operation__undo {
|
||||
background: url(../../assets/images/signature-undo.svg) no-repeat;
|
||||
}
|
||||
|
||||
.signature-operation__trash {
|
||||
background: url(../../assets/images/trash.svg) no-repeat;
|
||||
}
|
||||
|
||||
.signature-operation>div span {
|
||||
font-size: 12px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.signature-canvas {
|
||||
margin: 15px 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.signature-canvas canvas {
|
||||
background: #f3f5f7;
|
||||
}
|
||||
|
||||
.signature-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.signature-menu button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border: 1px solid #e2e6ed;
|
||||
border-radius: 2px;
|
||||
background: #ffffff;
|
||||
line-height: 22px;
|
||||
padding: 0 16px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.signature-menu button:hover {
|
||||
background: rgba(25, 55, 88, .04);
|
||||
}
|
||||
|
||||
.signature-menu__cancel {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.signature-menu button[type='submit'] {
|
||||
color: #ffffff;
|
||||
background: #4991f2;
|
||||
border-color: #4991f2;
|
||||
}
|
||||
|
||||
.signature-menu button[type='submit']:hover {
|
||||
background: #5b9cf3;
|
||||
border-color: #5b9cf3;
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
Reference in new issue
Block a user