Browse Source
# Conflicts: # src/components/DesignForm/assembly/index.ts # src/components/DesignForm/formControlAttr.vue # src/components/DesignForm/public/form/form.vue # src/components/DesignForm/public/form/formGroup.vue # src/views/sysworkflow/codepage/page.vue # src/widget/index.tsyjf_v2
21 changed files with 2099 additions and 5 deletions
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 10 KiB |
@ -0,0 +1,46 @@ |
|||||
|
//定义组合式API仓库
|
||||
|
import { defineStore } from "pinia"; |
||||
|
import { ref, computed,watch,reactive} from 'vue'; |
||||
|
import { VideoMsg } from '@/api/DesignForm/types' |
||||
|
/* type VideoObj = {} */ |
||||
|
|
||||
|
|
||||
|
//创建小仓库
|
||||
|
let uselowcodevideoStore = defineStore('lowcodevideo', () => { |
||||
|
|
||||
|
//视频地址
|
||||
|
//const videoResource = ref<string>();
|
||||
|
//是否上传成功
|
||||
|
//const videoReady = ref(false);
|
||||
|
//成功后接受的视频详细信息
|
||||
|
const videoMsg = reactive<VideoMsg>({ |
||||
|
CreatedAt: "", |
||||
|
UpdatedAt: "", |
||||
|
fileSize: 0, |
||||
|
id: 0, |
||||
|
key: "", |
||||
|
name: "", |
||||
|
physicspath: "", |
||||
|
size: "", |
||||
|
tag: "", |
||||
|
type: 0, |
||||
|
url: "", |
||||
|
videoReady: false, |
||||
|
videoAutoPlay: false, |
||||
|
attrId: "", |
||||
|
loop: false |
||||
|
}); |
||||
|
const videoMsgUse = reactive<VideoMsg[]>([]) |
||||
|
const videoOnShowIndex = ref(0);//当当前表单有多个视频控件时,字段配置栏目前所展示的视频属性在pinia数组中的索引,用来在formControlAttr.vue中的dom属性上绑定显示数据
|
||||
|
|
||||
|
//表单视频信息数组
|
||||
|
/* const videoArr = reactive<> */ |
||||
|
|
||||
|
//务必要返回一个对象:属性与方法可以提供给组件使用
|
||||
|
return { |
||||
|
videoOnShowIndex, |
||||
|
videoMsgUse, |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
export default uselowcodevideoStore; |
||||
@ -0,0 +1,19 @@ |
|||||
|
<!-- |
||||
|
@ 作者: 秦东 |
||||
|
@ 时间: 2023-12-07 16:49:57 |
||||
|
@ 备注: |
||||
|
--> |
||||
|
<template> |
||||
|
<baidu-map class="map" ak="ljiKlTAsS7SNVqDM16IUwRVFFhrvbxiF" v="3.0" :center="{lng: 116.404, lat: 39.915}" :zoom="15" :scroll-wheel-zoom="true"> |
||||
|
<bm-map-type :map-types="['BMAP_NORMAL_MAP', 'BMAP_HYBRID_MAP']" anchor="BMAP_ANCHOR_TOP_LEFT"></bm-map-type> |
||||
|
</baidu-map> |
||||
|
</template> |
||||
|
<script lang='ts' setup> |
||||
|
import { BaiduMap } from 'vue-baidu-map-3x' |
||||
|
</script> |
||||
|
<style lang='scss' scoped> |
||||
|
.map { |
||||
|
width: 100%; |
||||
|
height: calc(100vh - 90px); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,181 @@ |
|||||
|
<!-- |
||||
|
@ 作者: 李文轩 |
||||
|
@ 时间: 2024-01-02 13:49:57 |
||||
|
@ 备注: |
||||
|
--> |
||||
|
<template> |
||||
|
<el-form-item |
||||
|
v-bind="data.item" |
||||
|
:prop="tProp || data.name" |
||||
|
:class="config.className" |
||||
|
:rules="itemRules as any" |
||||
|
:label="getLabel(data.item as FormItem)" |
||||
|
> |
||||
|
<input v-model="value" type="hidden" > |
||||
|
</el-form-item> |
||||
|
|
||||
|
<LowcodeCarousel :data="props.data"></LowcodeCarousel> |
||||
|
</template> |
||||
|
<script lang='ts' setup> |
||||
|
import LowcodeCarousel from './lowcodeCarousel.vue'; |
||||
|
import { |
||||
|
constControlChange, |
||||
|
constFormProps, |
||||
|
} from '@/api/DesignForm/utils' |
||||
|
import validate from '@/api/DesignForm/validate' |
||||
|
import { FormItem, FormList } from '@/api/DesignForm/types' |
||||
|
const props = withDefaults( |
||||
|
defineProps<{ |
||||
|
data: FormList |
||||
|
tablekey: any |
||||
|
numrun?: number |
||||
|
modelValue?: any // 子表和弹性布局时时有传 |
||||
|
tProp?: string // 子表时的form-item的prop值,用于子表校验用 |
||||
|
}>(), |
||||
|
{} |
||||
|
) |
||||
|
const emits = defineEmits<{ |
||||
|
(e: 'update:modelValue', numVal: any): void |
||||
|
}>() |
||||
|
|
||||
|
const formProps = inject(constFormProps, {}) as any |
||||
|
const type = computed(() => { |
||||
|
return formProps.value.type |
||||
|
}) |
||||
|
const config = computed(() => { |
||||
|
return props.data.config || {} |
||||
|
}) |
||||
|
|
||||
|
const changeEvent = inject(constControlChange, '') as any |
||||
|
|
||||
|
const value = computed({ |
||||
|
get() { |
||||
|
if (props.tProp) { |
||||
|
// 表格和弹性布局 |
||||
|
return props.modelValue |
||||
|
} else { |
||||
|
return formProps.value.model[props.data.name] |
||||
|
} |
||||
|
}, |
||||
|
set(newVal: any) { |
||||
|
if (props.tProp) { |
||||
|
emits('update:modelValue', newVal) |
||||
|
} |
||||
|
updateModel(newVal) |
||||
|
} |
||||
|
}) |
||||
|
const updateModel = (val: any) => { |
||||
|
let controlAttribute = "" |
||||
|
if(props.data.control){ |
||||
|
if(props.data.control.type){ |
||||
|
controlAttribute = props.data.control.type |
||||
|
} |
||||
|
} |
||||
|
changeEvent && |
||||
|
changeEvent({ |
||||
|
key: props.data.name, |
||||
|
value: val, |
||||
|
data: props.data, |
||||
|
tProp: props.tProp, |
||||
|
type: props.data.type, |
||||
|
attribute: controlAttribute |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
const getLabel = (ele: FormItem) => { |
||||
|
const showColon = formProps.value.showColon ? ':' : '' |
||||
|
if (ele) { |
||||
|
return ele.showLabel ? '' : ele.label + showColon |
||||
|
} else { |
||||
|
return '' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 返回当前item项的校验规则 |
||||
|
const itemRules = computed(() => { |
||||
|
let temp |
||||
|
const itemR: any = props.data.item?.rules || [] |
||||
|
const customR = formatCustomRules() |
||||
|
// 如果三个都没有设置,则返回undefined |
||||
|
if (itemR?.length || customR?.length) { |
||||
|
temp = [...customR, ...itemR] |
||||
|
} |
||||
|
return temp |
||||
|
}) |
||||
|
// 处理自定义校验规则,将customRules转换后追加到rules里 |
||||
|
const formatCustomRules = () => { |
||||
|
const rulesReg: any = {} |
||||
|
validate && |
||||
|
validate.forEach(item => { |
||||
|
rulesReg[item.type] = item.regExp |
||||
|
}) |
||||
|
|
||||
|
// 获取校验方法 父级使用provide方法注入 |
||||
|
const temp: any = [] |
||||
|
props.data.customRules?.forEach((item: any) => { |
||||
|
if (!item.message && item.type !== 'methods') { |
||||
|
return // 方法时允许提示信息为空 |
||||
|
} |
||||
|
let obj = {} |
||||
|
if (item.type === 'required') { |
||||
|
obj = { required: true } |
||||
|
} else if (item.type === 'rules') { |
||||
|
// 自定义表达式 |
||||
|
obj = { pattern: item.rules } |
||||
|
} else if (item.type === 'methods') { |
||||
|
// 方法时 |
||||
|
const methods: any = item.methods |
||||
|
if (methods) { |
||||
|
obj = { validator: inject(methods, {}) } |
||||
|
} |
||||
|
} else if (item.type) { |
||||
|
obj = { pattern: rulesReg[item.type as string] } |
||||
|
} |
||||
|
// 这里判断下防某些条件下重复push的可能或存重复校验类型 |
||||
|
let message: any = { message: item.message } |
||||
|
if (!item.message) { |
||||
|
// 当使用validator校验时,如果存在message字段则不能使用 callback(new Error('x'));的提示 |
||||
|
message = {} |
||||
|
} |
||||
|
temp.push( |
||||
|
Object.assign( |
||||
|
{ |
||||
|
trigger: item.trigger || 'blur' |
||||
|
}, |
||||
|
obj, |
||||
|
message |
||||
|
) |
||||
|
) |
||||
|
}) |
||||
|
return temp |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
</script> |
||||
|
<style lang='scss' scoped> |
||||
|
.imgbox{ |
||||
|
padding: 0 5px; |
||||
|
max-width: 300px; |
||||
|
max-height: 200px; |
||||
|
width: 100%; |
||||
|
height: 200px; |
||||
|
.image-slot { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
background: var(--el-fill-color-light); |
||||
|
color: var(--el-text-color-secondary); |
||||
|
font-size: 30px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,112 @@ |
|||||
|
<template> |
||||
|
<el-carousel arrow="hover" :style="{height:carsuselHeight,width:carsuselWidth}" :interval = "interval" :height="carsuselHeight" trigger="click"> |
||||
|
<el-carousel-item v-for="item in carsouselData" :key="item.imgId"> |
||||
|
<el-image |
||||
|
style="width: 100%; height: 100%;cursor:pointer;" :src="item.imgUrl" fit="cover" alt="暂未上传" |
||||
|
@click="handleLink(item)"> |
||||
|
<template #error> |
||||
|
<!-- <div class="image-slot"> --> |
||||
|
<el-image style="width: 100%; height: 100%" :src="errimg" fit="fill" @click="noMsg" ></el-image> |
||||
|
<!-- </div> --> |
||||
|
<!-- <div class="image-slot"> |
||||
|
<el-icon><icon-picture /></el-icon> |
||||
|
</div> --> |
||||
|
</template> |
||||
|
</el-image> |
||||
|
</el-carousel-item> |
||||
|
</el-carousel> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { CarsuselConfig } from '@/api/DesignForm/types' |
||||
|
import { Picture as IconPicture } from '@element-plus/icons-vue' |
||||
|
import errimg from '@/assets/404_images/imgNotFound.png' |
||||
|
//import errimg from '@/assets/404_images/untilUploadImg.png' |
||||
|
|
||||
|
const props = defineProps({ |
||||
|
// eslint-disable-next-line vue/require-default-prop |
||||
|
data: { |
||||
|
type: Object, |
||||
|
} |
||||
|
}) |
||||
|
const carsouselData: CarsuselConfig[] = props.data?.control.carsuselConfigArr |
||||
|
|
||||
|
const carsuselHeight = props.data?.control.config.carsuselHeight+'px' |
||||
|
|
||||
|
const carsuselWidth = props.data?.control.config.carsuselWidth+'px' |
||||
|
|
||||
|
const interval = props.data?.control.config.interval |
||||
|
|
||||
|
function errorImg(e: any) { |
||||
|
e.srcElement.src = errimg; |
||||
|
//这一句没用,如果默认图片的路径错了还是会一直闪屏,在方法的前面加个.once只让它执行一次也没用 |
||||
|
e.srcElement.onerror = null; //防止闪图 |
||||
|
} |
||||
|
function handleLink(item: any) { |
||||
|
console.log("handleLink") |
||||
|
let url = ""; |
||||
|
let urlStart = 'http://' |
||||
|
// http:// 7 |
||||
|
//https:// 8 |
||||
|
if (item.link.length < 7) { |
||||
|
if (item.link == '') { |
||||
|
alert("未配置跳转地址") |
||||
|
return |
||||
|
} |
||||
|
url = urlStart + "" + item.link |
||||
|
} else { |
||||
|
const linkStartComplete1 = item.link.startsWith("http://") |
||||
|
const linkStartComplete2 = item.link.startsWith("https://") |
||||
|
if (linkStartComplete1 || linkStartComplete2) { |
||||
|
url = item.link |
||||
|
} else { |
||||
|
url = urlStart + "" + item.link |
||||
|
} |
||||
|
} |
||||
|
window.open(url, '_blank') |
||||
|
} |
||||
|
function noMsg() { |
||||
|
alert("轮播图未配置") |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.image-slot { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
background: var(--el-fill-color-light); |
||||
|
color: var(--el-text-color-secondary); |
||||
|
font-size: 30px; |
||||
|
} |
||||
|
|
||||
|
.demonstration { |
||||
|
color: var(--el-text-color-secondary); |
||||
|
} |
||||
|
|
||||
|
.el-carousel__item h3 { |
||||
|
color: black; |
||||
|
opacity: 0.75; |
||||
|
line-height: 150px; |
||||
|
margin: 0; |
||||
|
text-align: center; |
||||
|
} |
||||
|
|
||||
|
.el-carousel__item:nth-child(2n) { |
||||
|
background-color: #99a9bf; |
||||
|
} |
||||
|
|
||||
|
.el-carousel__item:nth-child(2n + 1) { |
||||
|
background-color: #d3dce6; |
||||
|
} |
||||
|
|
||||
|
/* .el-carousel__item:nth-child(2n) { |
||||
|
background-color: white; |
||||
|
} |
||||
|
|
||||
|
.el-carousel__item:nth-child(2n + 1) { |
||||
|
background-color: white; |
||||
|
} */ |
||||
|
</style> |
||||
@ -0,0 +1,181 @@ |
|||||
|
<!-- |
||||
|
@ 作者: 李文轩 |
||||
|
@ 时间: 2024-01-02 13:49:57 |
||||
|
@ 备注: |
||||
|
--> |
||||
|
<template> |
||||
|
<el-form-item |
||||
|
v-bind="data.item" |
||||
|
:prop="tProp || data.name" |
||||
|
:class="config.className" |
||||
|
:rules="itemRules as any" |
||||
|
:label="getLabel(data.item as FormItem)" |
||||
|
> |
||||
|
<input v-model="value" type="hidden" > |
||||
|
</el-form-item> |
||||
|
|
||||
|
<VideoUploadPlay :data="props.data"></VideoUploadPlay> |
||||
|
</template> |
||||
|
<script lang='ts' setup> |
||||
|
import VideoUploadPlay from './videoUploadPlay.vue'; |
||||
|
import { |
||||
|
constControlChange, |
||||
|
constFormProps, |
||||
|
} from '@/api/DesignForm/utils' |
||||
|
import validate from '@/api/DesignForm/validate' |
||||
|
import { FormItem, FormList } from '@/api/DesignForm/types' |
||||
|
const props = withDefaults( |
||||
|
defineProps<{ |
||||
|
data: FormList |
||||
|
tablekey: any |
||||
|
numrun?: number |
||||
|
modelValue?: any // 子表和弹性布局时时有传 |
||||
|
tProp?: string // 子表时的form-item的prop值,用于子表校验用 |
||||
|
}>(), |
||||
|
{} |
||||
|
) |
||||
|
const emits = defineEmits<{ |
||||
|
(e: 'update:modelValue', numVal: any): void |
||||
|
}>() |
||||
|
|
||||
|
const formProps = inject(constFormProps, {}) as any |
||||
|
const type = computed(() => { |
||||
|
return formProps.value.type |
||||
|
}) |
||||
|
const config = computed(() => { |
||||
|
return props.data.config || {} |
||||
|
}) |
||||
|
|
||||
|
const changeEvent = inject(constControlChange, '') as any |
||||
|
|
||||
|
const value = computed({ |
||||
|
get() { |
||||
|
if (props.tProp) { |
||||
|
// 表格和弹性布局 |
||||
|
return props.modelValue |
||||
|
} else { |
||||
|
return formProps.value.model[props.data.name] |
||||
|
} |
||||
|
}, |
||||
|
set(newVal: any) { |
||||
|
if (props.tProp) { |
||||
|
emits('update:modelValue', newVal) |
||||
|
} |
||||
|
updateModel(newVal) |
||||
|
} |
||||
|
}) |
||||
|
const updateModel = (val: any) => { |
||||
|
let controlAttribute = "" |
||||
|
if(props.data.control){ |
||||
|
if(props.data.control.type){ |
||||
|
controlAttribute = props.data.control.type |
||||
|
} |
||||
|
} |
||||
|
changeEvent && |
||||
|
changeEvent({ |
||||
|
key: props.data.name, |
||||
|
value: val, |
||||
|
data: props.data, |
||||
|
tProp: props.tProp, |
||||
|
type: props.data.type, |
||||
|
attribute: controlAttribute |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
const getLabel = (ele: FormItem) => { |
||||
|
const showColon = formProps.value.showColon ? ':' : '' |
||||
|
if (ele) { |
||||
|
return ele.showLabel ? '' : ele.label + showColon |
||||
|
} else { |
||||
|
return '' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 返回当前item项的校验规则 |
||||
|
const itemRules = computed(() => { |
||||
|
let temp |
||||
|
const itemR: any = props.data.item?.rules || [] |
||||
|
const customR = formatCustomRules() |
||||
|
// 如果三个都没有设置,则返回undefined |
||||
|
if (itemR?.length || customR?.length) { |
||||
|
temp = [...customR, ...itemR] |
||||
|
} |
||||
|
return temp |
||||
|
}) |
||||
|
// 处理自定义校验规则,将customRules转换后追加到rules里 |
||||
|
const formatCustomRules = () => { |
||||
|
const rulesReg: any = {} |
||||
|
validate && |
||||
|
validate.forEach(item => { |
||||
|
rulesReg[item.type] = item.regExp |
||||
|
}) |
||||
|
|
||||
|
// 获取校验方法 父级使用provide方法注入 |
||||
|
const temp: any = [] |
||||
|
props.data.customRules?.forEach((item: any) => { |
||||
|
if (!item.message && item.type !== 'methods') { |
||||
|
return // 方法时允许提示信息为空 |
||||
|
} |
||||
|
let obj = {} |
||||
|
if (item.type === 'required') { |
||||
|
obj = { required: true } |
||||
|
} else if (item.type === 'rules') { |
||||
|
// 自定义表达式 |
||||
|
obj = { pattern: item.rules } |
||||
|
} else if (item.type === 'methods') { |
||||
|
// 方法时 |
||||
|
const methods: any = item.methods |
||||
|
if (methods) { |
||||
|
obj = { validator: inject(methods, {}) } |
||||
|
} |
||||
|
} else if (item.type) { |
||||
|
obj = { pattern: rulesReg[item.type as string] } |
||||
|
} |
||||
|
// 这里判断下防某些条件下重复push的可能或存重复校验类型 |
||||
|
let message: any = { message: item.message } |
||||
|
if (!item.message) { |
||||
|
// 当使用validator校验时,如果存在message字段则不能使用 callback(new Error('x'));的提示 |
||||
|
message = {} |
||||
|
} |
||||
|
temp.push( |
||||
|
Object.assign( |
||||
|
{ |
||||
|
trigger: item.trigger || 'blur' |
||||
|
}, |
||||
|
obj, |
||||
|
message |
||||
|
) |
||||
|
) |
||||
|
}) |
||||
|
return temp |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
</script> |
||||
|
<style lang='scss' scoped> |
||||
|
.imgbox{ |
||||
|
padding: 0 5px; |
||||
|
max-width: 300px; |
||||
|
max-height: 200px; |
||||
|
width: 100%; |
||||
|
height: 200px; |
||||
|
.image-slot { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
background: var(--el-fill-color-light); |
||||
|
color: var(--el-text-color-secondary); |
||||
|
font-size: 30px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,32 @@ |
|||||
|
<template> |
||||
|
<!-- <p>{{ videoHeight }}</p> |
||||
|
<p>{{ videoWidth }}</p> --> |
||||
|
<video |
||||
|
:src="data?.control.videoMsg[0].url" |
||||
|
:loop="data?.control.videoMsg[0].loop" |
||||
|
:autoplay="data?.control.videoMsg[0].videoAutoPlay" |
||||
|
:style="{height:videoHeight,width:videoWidth}" |
||||
|
controls |
||||
|
> |
||||
|
</video> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
|
||||
|
|
||||
|
const props = defineProps({ |
||||
|
// eslint-disable-next-line vue/require-default-prop |
||||
|
data: { |
||||
|
type: Object, |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
const videoHeight = props.data?.control.videoMsg[0].videoHeight+'px' |
||||
|
|
||||
|
const videoWidth = props.data?.control.videoMsg[0].videoWidth+'px' |
||||
|
|
||||
|
|
||||
|
|
||||
|
</script> |
||||
|
|
||||
|
<style scoped></style> |
||||
@ -0,0 +1,186 @@ |
|||||
|
<!-- |
||||
|
@ 作者: 秦东 |
||||
|
@ 时间: 2023-12-08 16:49:57 |
||||
|
@ 备注: |
||||
|
--> |
||||
|
<template> |
||||
|
<el-form-item |
||||
|
v-bind="data.item" |
||||
|
:prop="tProp || data.name" |
||||
|
:class="config.className" |
||||
|
:rules="itemRules as any" |
||||
|
:label="getLabel(data.item as FormItem)" |
||||
|
> |
||||
|
<input v-model="value" type="hidden" > |
||||
|
</el-form-item> |
||||
|
|
||||
|
<PaintBoard @updataconbt="qianming" /> |
||||
|
</template> |
||||
|
<script lang='ts' setup> |
||||
|
import PaintBoard from './paintBoard.vue'; |
||||
|
import { |
||||
|
constControlChange, |
||||
|
constFormProps, |
||||
|
} from '@/api/DesignForm/utils' |
||||
|
import validate from '@/api/DesignForm/validate' |
||||
|
import { FormItem, FormList } from '@/api/DesignForm/types' |
||||
|
const props = withDefaults( |
||||
|
defineProps<{ |
||||
|
data: FormList |
||||
|
tablekey: any |
||||
|
numrun?: number |
||||
|
modelValue?: any // 子表和弹性布局时时有传 |
||||
|
tProp?: string // 子表时的form-item的prop值,用于子表校验用 |
||||
|
}>(), |
||||
|
{} |
||||
|
) |
||||
|
const emits = defineEmits<{ |
||||
|
(e: 'update:modelValue', numVal: any): void |
||||
|
}>() |
||||
|
|
||||
|
const formProps = inject(constFormProps, {}) as any |
||||
|
const type = computed(() => { |
||||
|
return formProps.value.type |
||||
|
}) |
||||
|
const config = computed(() => { |
||||
|
return props.data.config || {} |
||||
|
}) |
||||
|
|
||||
|
const changeEvent = inject(constControlChange, '') as any |
||||
|
|
||||
|
const value = computed({ |
||||
|
get() { |
||||
|
if (props.tProp) { |
||||
|
// 表格和弹性布局 |
||||
|
return props.modelValue |
||||
|
} else { |
||||
|
return formProps.value.model[props.data.name] |
||||
|
} |
||||
|
}, |
||||
|
set(newVal: any) { |
||||
|
if (props.tProp) { |
||||
|
emits('update:modelValue', newVal) |
||||
|
} |
||||
|
updateModel(newVal) |
||||
|
} |
||||
|
}) |
||||
|
const updateModel = (val: any) => { |
||||
|
let controlAttribute = "" |
||||
|
if(props.data.control){ |
||||
|
if(props.data.control.type){ |
||||
|
controlAttribute = props.data.control.type |
||||
|
} |
||||
|
} |
||||
|
changeEvent && |
||||
|
changeEvent({ |
||||
|
key: props.data.name, |
||||
|
value: val, |
||||
|
data: props.data, |
||||
|
tProp: props.tProp, |
||||
|
type: props.data.type, |
||||
|
attribute: controlAttribute |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
const getLabel = (ele: FormItem) => { |
||||
|
const showColon = formProps.value.showColon ? ':' : '' |
||||
|
if (ele) { |
||||
|
return ele.showLabel ? '' : ele.label + showColon |
||||
|
} else { |
||||
|
return '' |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 返回当前item项的校验规则 |
||||
|
const itemRules = computed(() => { |
||||
|
let temp |
||||
|
const itemR: any = props.data.item?.rules || [] |
||||
|
const customR = formatCustomRules() |
||||
|
// 如果三个都没有设置,则返回undefined |
||||
|
if (itemR?.length || customR?.length) { |
||||
|
temp = [...customR, ...itemR] |
||||
|
} |
||||
|
return temp |
||||
|
}) |
||||
|
// 处理自定义校验规则,将customRules转换后追加到rules里 |
||||
|
const formatCustomRules = () => { |
||||
|
const rulesReg: any = {} |
||||
|
validate && |
||||
|
validate.forEach(item => { |
||||
|
rulesReg[item.type] = item.regExp |
||||
|
}) |
||||
|
|
||||
|
// 获取校验方法 父级使用provide方法注入 |
||||
|
const temp: any = [] |
||||
|
props.data.customRules?.forEach((item: any) => { |
||||
|
if (!item.message && item.type !== 'methods') { |
||||
|
return // 方法时允许提示信息为空 |
||||
|
} |
||||
|
let obj = {} |
||||
|
if (item.type === 'required') { |
||||
|
obj = { required: true } |
||||
|
} else if (item.type === 'rules') { |
||||
|
// 自定义表达式 |
||||
|
obj = { pattern: item.rules } |
||||
|
} else if (item.type === 'methods') { |
||||
|
// 方法时 |
||||
|
const methods: any = item.methods |
||||
|
if (methods) { |
||||
|
obj = { validator: inject(methods, {}) } |
||||
|
} |
||||
|
} else if (item.type) { |
||||
|
obj = { pattern: rulesReg[item.type as string] } |
||||
|
} |
||||
|
// 这里判断下防某些条件下重复push的可能或存重复校验类型 |
||||
|
let message: any = { message: item.message } |
||||
|
if (!item.message) { |
||||
|
// 当使用validator校验时,如果存在message字段则不能使用 callback(new Error('x'));的提示 |
||||
|
message = {} |
||||
|
} |
||||
|
temp.push( |
||||
|
Object.assign( |
||||
|
{ |
||||
|
trigger: item.trigger || 'blur' |
||||
|
}, |
||||
|
obj, |
||||
|
message |
||||
|
) |
||||
|
) |
||||
|
}) |
||||
|
return temp |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
const qianming = (val:any) =>{ |
||||
|
console.log("图片回传--->",val) |
||||
|
|
||||
|
value.value = val |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
|
<style lang='scss' scoped> |
||||
|
.imgbox{ |
||||
|
padding: 0 5px; |
||||
|
max-width: 300px; |
||||
|
max-height: 200px; |
||||
|
width: 100%; |
||||
|
height: 200px; |
||||
|
.image-slot { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
background: var(--el-fill-color-light); |
||||
|
color: var(--el-text-color-secondary); |
||||
|
font-size: 30px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,124 @@ |
|||||
|
<template> |
||||
|
<div > |
||||
|
<div v-show="pantareaisShow" id="canvasWrap" style="width:100%;"> |
||||
|
<vueOnlineSignature ref="vueSignatureRef" v-bind="params"/> |
||||
|
</div> |
||||
|
<img v-if="imagesSRC" :src="imagesSRC" alt="" style="max-width: 100%"> |
||||
|
<div class="buttonList" > |
||||
|
<div class="button" @click="confirm">签名完成</div> |
||||
|
<div class="button" @click="reset">重签</div> |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import vueOnlineSignature from '@/widget/writingboard/vueSignature.vue'; |
||||
|
import { ref, reactive } from 'vue' |
||||
|
|
||||
|
const pantareaisShow = ref<boolean>(true) |
||||
|
|
||||
|
const params = reactive<any>({ |
||||
|
width: 751,//签名板宽 生成的图片长宽像素都会变小1px,需在此多设置1px |
||||
|
height: 301,//签名板长 |
||||
|
lineWidth: 5, |
||||
|
lineColor: '#000', |
||||
|
canvasBack: new URL('@/assets/paintboard.png', import.meta.url).href,//签名板背景 |
||||
|
isCrop: true, |
||||
|
edg: 0, |
||||
|
fullScreen: false, |
||||
|
domId: '', |
||||
|
imgType: 'image/png', |
||||
|
imgBack: new URL('@/assets/paintboard.png', import.meta.url).href,//生成签名背景 |
||||
|
isRepeat: '', |
||||
|
noRotation: false, |
||||
|
backIsCenter: false, |
||||
|
verticalDeductWidth: 10, |
||||
|
verticalDeductHeight: 24, |
||||
|
acrossDeductWidth: 30, |
||||
|
acrossDeductHeight: 20, |
||||
|
recoverPoints: [], |
||||
|
isBrush: false, |
||||
|
brushLine: 20 |
||||
|
}) |
||||
|
|
||||
|
const emits = defineEmits(["updataconbt"]); |
||||
|
|
||||
|
let vueSignatureRef = ref<any>(null) |
||||
|
const imagesSRC = ref<string>('') |
||||
|
const confirm = () => { |
||||
|
|
||||
|
vueSignatureRef.value.confirm() |
||||
|
.then((res:{base64: string, points: any}) => { |
||||
|
imagesSRC.value = res.base64 |
||||
|
sessionStorage.setItem('points', JSON.stringify(res.points)) |
||||
|
pantareaisShow.value = false |
||||
|
emits("updataconbt",res.base64) |
||||
|
}) |
||||
|
.catch(() => { |
||||
|
alert('未曾签名') |
||||
|
}) |
||||
|
} |
||||
|
const reset = () => { |
||||
|
imagesSRC.value = '' |
||||
|
pantareaisShow.value = true |
||||
|
if(vueSignatureRef.value != null){ |
||||
|
vueSignatureRef.value.reset() |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
</script> |
||||
|
|
||||
|
<style lang="less" scoped> |
||||
|
#canvasWrap{ |
||||
|
margin: 1px 0; |
||||
|
} |
||||
|
canvas{ |
||||
|
border: 1px dashed #ccc |
||||
|
} |
||||
|
.input__wrap{ |
||||
|
list-style-type: none; |
||||
|
width: 100%; |
||||
|
padding: 0; |
||||
|
li{ |
||||
|
line-height: 30px; |
||||
|
display: flex; |
||||
|
justify-content: flex-start; |
||||
|
align-items: center; |
||||
|
flex-wrap: wrap; |
||||
|
} |
||||
|
span{ |
||||
|
width: 130px; |
||||
|
display: inline-block; |
||||
|
text-align: right; |
||||
|
margin-right: 10px |
||||
|
} |
||||
|
input[type="checkbox"]{ |
||||
|
margin-left: 0 |
||||
|
} |
||||
|
p{ |
||||
|
margin: 0 0 10px 140px; |
||||
|
color:#aaa |
||||
|
} |
||||
|
} |
||||
|
.button{ |
||||
|
width: 290px; |
||||
|
height: 35px; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
font-size: 15px; |
||||
|
border: 1px solid #ccc; |
||||
|
border-radius: 5px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
.buttonList{ |
||||
|
display: flex; |
||||
|
.button ~ .button{ |
||||
|
margin-left: 10px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,713 @@ |
|||||
|
<template> |
||||
|
<canvas v-if="esignReset" ref="canvasRef" @mousedown="onMouseDown" @mousemove="onMouseMove" @mouseup="onMouseUp" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd"></canvas> |
||||
|
<!-- <img v-show="false" ref="penRef" :src="penImg" > --> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { ref, reactive, watch, computed, onMounted, nextTick, onBeforeMount, onUnmounted, toRaw } from 'vue' |
||||
|
const emits = defineEmits(['onDrawingStatus', 'onMouseDown', 'onMouseMove', 'onMouseUp', 'onTouchStart', 'onTouchMove', 'onTouchEnd']) |
||||
|
interface pointsType { |
||||
|
x: number, |
||||
|
y: number, |
||||
|
direction: string |
||||
|
} |
||||
|
interface Props { |
||||
|
width?: number, |
||||
|
height?: number, |
||||
|
lineWidth?: number, |
||||
|
lineColor?: string, |
||||
|
canvasBack?: string, |
||||
|
isCrop?: boolean, |
||||
|
edg?: number, |
||||
|
fullScreen?: boolean, |
||||
|
domId?: string, |
||||
|
imgBack?: string, |
||||
|
isRepeat?: string, |
||||
|
noRotation?: boolean, |
||||
|
imgType?: string, |
||||
|
backIsCenter?: boolean, |
||||
|
acrossDeductWidth?: number, |
||||
|
acrossDeductHeight?: number |
||||
|
verticalDeductWidth?: number, |
||||
|
verticalDeductHeight?: number, |
||||
|
recoverPoints?: pointsType[], |
||||
|
isBrush?: boolean, |
||||
|
brushLine?: number |
||||
|
} |
||||
|
const props = withDefaults(defineProps<Props>(), { |
||||
|
width: 0, // 画布宽度,优先级三级 (此值不可大于当前屏幕的宽度) |
||||
|
height: 0, // 画布高度,优先级三级 |
||||
|
lineWidth: 8, // 画笔粗细 |
||||
|
lineColor: '#000000', // 画笔颜色 |
||||
|
canvasBack: '', // 画布背景,为空时画布背景透明,支持多种格式 '#ccc','#E5A1A1','rgb(229, 161, 161)','rgba(0,0,0,.6)','red', 'http'、'https'、文件路径及'base64'类型图片链接 |
||||
|
isCrop: false, // 是否裁剪,在画布设定尺寸基础上裁掉四周空白部分 |
||||
|
edg: 0, // 画布导出图需要旋转的角度,必须是90的倍数(如竖屏导出图会生成竖屏尺寸的图片,此参数值为270时,会生成一张横向的图片) |
||||
|
fullScreen: false, // 是否获取屏幕的宽高生成画布尺寸,优先级二级 |
||||
|
domId: '', // 用于获取元素的宽高生成画布尺寸,优先级一级(建议使用canvas父级元素的ID, 父级元素的width值不可大于当前屏幕的宽度) |
||||
|
imgBack: '', //画布最终导出图的图片背景,如果此参数不为空,生成图片时会覆盖canvasBack的背景图,支持多种格式 '#ccc','#E5A1A1','rgb(229, 161, 161)','rgba(0,0,0,.6)','red', 'http'、'https'、文件路径及'base64'类型图片链接 |
||||
|
isRepeat: '', // 画布背景是否重复(参数:'repeat','repeat-x','repeat-y' ) |
||||
|
noRotation: false, // 横屏时导出图是否旋转角度 (当值为true时,横屏时导出图不会旋转角度) |
||||
|
imgType: 'image/png', // 画布导出图的图片类型(可以是其他'image/jpeg'等) |
||||
|
backIsCenter: false, // 背景图片是否居中显示(使用domId或输入固定宽度时生效并且只有图片宽度大于canvas宽度才会生效) |
||||
|
verticalDeductWidth: 0, // 获取屏幕的宽高生成画布尺时,竖屏时宽度需要减除的尺寸 |
||||
|
verticalDeductHeight: 0, // 获取屏幕的宽高生成画布尺时,竖屏时高度需要减除的尺寸 |
||||
|
acrossDeductWidth: 0, // 获取屏幕的宽高生成画布尺时,横屏时宽度需要减除的尺寸 |
||||
|
acrossDeductHeight: 0, // 获取屏幕的宽高生成画布尺时,横屏时高度需要减除的尺寸 |
||||
|
recoverPoints: () => [], // 初始生成布画时,需要恢复到canvas画布上的笔画数据(此数据结构必须是confirm方法返回的结构,结构:[{x:0,y:0,direction:'across'}], direction参数有across和vertical) |
||||
|
isBrush: false, // 是否使用毛笔字画笔(开启后,imgBack以及屏幕转旋记录笔画功能无法生效) |
||||
|
brushLine: 20 // 毛笔画笔笔线尺寸,最小值20,isBrush为true时生效 |
||||
|
}) |
||||
|
const hasDrew = ref<boolean>(false) |
||||
|
const isOrientationchange = ref<boolean>(false) |
||||
|
const esignReset = ref<boolean>(true) |
||||
|
const resultImg = ref<string>('') |
||||
|
const points = ref<any[]>([]) |
||||
|
let canvasTxt = ref<CanvasRenderingContext2D | null>(null) |
||||
|
let canvasRef = ref<HTMLCanvasElement | null>(null) |
||||
|
let cropCanvas = ref<HTMLCanvasElement | null>(null) |
||||
|
let cropCanvasTxt = ref<CanvasRenderingContext2D | null>(null) |
||||
|
const startX = ref<number>(0) |
||||
|
const startY = ref<number>(0) |
||||
|
const sratio = ref<number>(1) |
||||
|
const isDrawing = ref<boolean>(false) |
||||
|
const isLoad = ref<boolean>(false) |
||||
|
let imgBackDom = ref<any>(null) |
||||
|
let canvasBackDom = ref<any>(null) |
||||
|
let screenPatams = reactive<{ width:number, height: number }>({ |
||||
|
width: 0, |
||||
|
height: 0 |
||||
|
}) |
||||
|
let domPatams = reactive<{ width:number, height: number }>({ |
||||
|
width: 0, |
||||
|
height: 0 |
||||
|
}) |
||||
|
// 毛笔新加 -------------------------------- |
||||
|
const hasPoints = ref<any[]>([]) |
||||
|
const pointsArr = ref<any[]>([]) |
||||
|
const smoothness = ref<number>(80) |
||||
|
const l = ref<number>(props.brushLine < 20 ? 20 : props.brushLine) |
||||
|
//const penImg = new URL('@/assets/pen2.png', import.meta.url).href |
||||
|
let penRef = ref<any>(null) |
||||
|
// 毛笔新加 -------------------------------- |
||||
|
|
||||
|
const ratio = computed(() => (domPatams.height ? domPatams.height : props.fullScreen ? screenPatams.height : props.height) / (domPatams.width ? domPatams.width : props.fullScreen ? screenPatams.width : props.width) ) |
||||
|
const canvasBackground = computed(() => props.canvasBack ? props.canvasBack : 'rgba(255, 255, 255, 0)' ) |
||||
|
watch(canvasBackground, async (newVal: string) => { |
||||
|
await nextTick() |
||||
|
canvasRef.value && (canvasRef.value.style.background = newVal) |
||||
|
}) |
||||
|
watch(hasDrew, (newVal: boolean) => { |
||||
|
emits('onDrawingStatus', newVal) |
||||
|
}) |
||||
|
const getSizeRatio = () => { |
||||
|
return !props.fullScreen && props.backIsCenter |
||||
|
} |
||||
|
const setCanvasImageBack = (status: any) => { |
||||
|
const canvas = canvasRef.value as HTMLCanvasElement |
||||
|
let pat = canvasTxt.value?.createPattern(canvasBackDom.value, (props.isRepeat || "no-repeat")); |
||||
|
canvasTxt.value?.rect(0,0,canvas.width ,canvas.height) |
||||
|
canvasTxt!.value!.fillStyle = (pat as any); |
||||
|
canvasTxt.value?.fill(); |
||||
|
if (status) { |
||||
|
autoDraw(null, null) |
||||
|
} |
||||
|
} |
||||
|
const setCanvasBack = (status: any) => { |
||||
|
const canvas = canvasRef.value as HTMLCanvasElement |
||||
|
if (props.canvasBack && canvasBackDom.value && isImgaes(props.canvasBack)) { |
||||
|
setCanvasImageBack(status) |
||||
|
} else { |
||||
|
canvas.style.background = canvasBackground.value |
||||
|
} |
||||
|
} |
||||
|
const getDomSize = () => { |
||||
|
const canvas = canvasRef.value as HTMLCanvasElement |
||||
|
if (props.domId) { |
||||
|
let dom = document.getElementById(props.domId) |
||||
|
let domWidth = dom ? dom.clientWidth || dom.offsetWidth : props.fullScreen ? screenPatams.width : props.width |
||||
|
let domHeight = dom ? dom.clientHeight || dom.offsetHeight : props.fullScreen ? screenPatams.height : props.height |
||||
|
canvas.height = domHeight |
||||
|
canvas.width = domWidth |
||||
|
domPatams.width = domWidth |
||||
|
domPatams.height = domHeight |
||||
|
} else { |
||||
|
canvas.height = props.fullScreen ? screenPatams.height : props.height |
||||
|
canvas.width = props.fullScreen ? screenPatams.width : props.width |
||||
|
} |
||||
|
} |
||||
|
const resizeHandler = (status: any) => { |
||||
|
if (isOrientationchange.value) return false |
||||
|
const canvas = canvasRef.value as HTMLCanvasElement |
||||
|
canvas.style.width = (domPatams.width ? domPatams.width : props.fullScreen ? screenPatams.width : props.width) + "px" |
||||
|
const realw = parseFloat(window.getComputedStyle(canvas).width) |
||||
|
canvas.style.height = ratio.value * realw + "px"; |
||||
|
canvasTxt.value = canvas.getContext('2d') |
||||
|
canvasTxt.value?.scale(1 * sratio.value, 1 * sratio.value) |
||||
|
sratio.value = realw / (domPatams.width ? domPatams.width : props.fullScreen ? screenPatams.width : props.width) |
||||
|
canvasTxt.value?.scale(1 / sratio.value, 1 / sratio.value) |
||||
|
if (props.canvasBack) { |
||||
|
let IntervaId = setInterval(() => { |
||||
|
if ((canvasBackDom.value && isLoad.value) || !isImgaes(props.canvasBack)) { |
||||
|
setCanvasBack(status) |
||||
|
clearInterval(IntervaId) |
||||
|
} |
||||
|
}, 100) |
||||
|
} else { |
||||
|
if (status) { |
||||
|
autoDraw(null, null) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
const orientationchangeEvent = () => { |
||||
|
let directionWidth = window.orientation == 0 || window.orientation == 180 ? props.verticalDeductWidth : props.acrossDeductWidth |
||||
|
let directionHeight = window.orientation == 0 || window.orientation == 180 ? props.verticalDeductHeight : props.acrossDeductHeight |
||||
|
screenPatams.width = window.navigator.platform.indexOf('Win') || window.navigator.platform.indexOf('Mac') ? (document.body.clientHeight || document.body.offsetHeight) - directionWidth : (document.body.clientWidth || document.body.offsetWidth) - directionWidth |
||||
|
screenPatams.height = window.navigator.platform.indexOf('Win') || window.navigator.platform.indexOf('Mac') ? (document.body.clientWidth || document.body.offsetWidth) - directionHeight : (document.body.clientHeight || document.body.offsetHeight) - directionHeight |
||||
|
getImages() |
||||
|
isOrientationchange.value = true |
||||
|
esignReset.value = false |
||||
|
let setIntervalId = setInterval(async() => { |
||||
|
if (isLoad.value) { |
||||
|
clearInterval(setIntervalId) |
||||
|
esignReset.value = true |
||||
|
isOrientationchange.value = false |
||||
|
await nextTick() |
||||
|
getDomSize() |
||||
|
resizeHandler(true) |
||||
|
} |
||||
|
}, 100) |
||||
|
} |
||||
|
const isImgaes = (params: string) => { |
||||
|
let imgType = ['.jpeg', '.bmp', '.jpg', '.gif', '.webp', '.pcx', '.tif', '.tga', '.exif', '.fpx', '.svg', '.cdr', '.pcd', '.dxf', '.ufo', '.eps', '.ai', '.png', '.hdri', '.raw', '.wmf', '.flic', '.emf', '.ico', '.avif', '.apng'] |
||||
|
let regex = /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*?)\s*$/i; |
||||
|
let status = params.includes('http://') || params.includes('https://') || regex.test(params) || imgType.some(item => params.includes(item)) |
||||
|
return status |
||||
|
} |
||||
|
const rotateBase64Img = (src: string, edg: number, type: string = 'not') => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
let canvas: HTMLCanvasElement = document.createElement("canvas"); |
||||
|
let ctx = canvas.getContext("2d") as CanvasRenderingContext2D |
||||
|
let imgW;//图片宽度 |
||||
|
let imgH;//图片高度 |
||||
|
let size;//canvas初始大小 |
||||
|
if (edg % 90 != 0) { |
||||
|
reject("旋转角度必须是90的倍数!"); |
||||
|
throw '旋转角度必须是90的倍数!'; |
||||
|
} |
||||
|
(edg < 0) && (edg = (edg % 360) + 360) |
||||
|
const quadrant = (edg / 90) % 4; //旋转象限 |
||||
|
const cutCoor = {sx: 0, sy: 0, ex: 0, ey: 0}; //裁剪坐标 |
||||
|
let image = new Image(); |
||||
|
image.crossOrigin = "anonymous" |
||||
|
image.src = src; |
||||
|
image.onload = function () { |
||||
|
imgW = image.width; |
||||
|
imgH = image.height; |
||||
|
//console.log(imgH, 'imgH') |
||||
|
size = imgW > imgH ? imgW : imgH; |
||||
|
canvas.width = size * 2; |
||||
|
canvas.height = size * 2; |
||||
|
let Cwidth = domPatams.width ? domPatams.width : props.fullScreen ? screenPatams.width : props.width |
||||
|
let ratio = getSizeRatio() && type == 'init' |
||||
|
switch (quadrant) { |
||||
|
case 0: |
||||
|
cutCoor.sx = getSizeRatio() && type == 'init' && imgW > screenPatams.width ? size - ((imgW - (getDirection() == 'across' ? screenPatams.width : screenPatams.height)) / 2) : size |
||||
|
cutCoor.sy = size; |
||||
|
cutCoor.ex = size + imgW; |
||||
|
cutCoor.ey = size + imgH; |
||||
|
break; |
||||
|
case 1: |
||||
|
cutCoor.sx = ratio ? size - props.height : window.orientation == 0 || window.orientation == 180 ? size - Cwidth : size - imgH |
||||
|
cutCoor.sy = size |
||||
|
cutCoor.ex = size; |
||||
|
cutCoor.ey = size + imgW; |
||||
|
break; |
||||
|
case 2: |
||||
|
cutCoor.sx = size - imgW; |
||||
|
cutCoor.sy = size - imgH; |
||||
|
cutCoor.ex = size; |
||||
|
cutCoor.ey = size; |
||||
|
break; |
||||
|
case 3: |
||||
|
cutCoor.sx = size; |
||||
|
cutCoor.sy = size - imgW; |
||||
|
cutCoor.ex = size + imgH; |
||||
|
cutCoor.ey = size + imgW; |
||||
|
break; |
||||
|
} |
||||
|
ctx.translate(size, size); |
||||
|
ctx.rotate(edg * Math.PI / 180); |
||||
|
ctx.drawImage(image, 0, 0); |
||||
|
var imgData = ctx.getImageData(cutCoor.sx, cutCoor.sy, cutCoor.ex, cutCoor.ey); |
||||
|
if (quadrant % 2 == 0) { |
||||
|
canvas.width = imgW; |
||||
|
canvas.height = imgH; |
||||
|
} else { |
||||
|
canvas.width = imgH; |
||||
|
canvas.height = imgW; |
||||
|
} |
||||
|
//putImageData() 将图像数据放回画布 |
||||
|
ctx.putImageData(imgData, 0, 0); |
||||
|
resolve(canvas.toDataURL(props.imgType)) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
const getImages = async () => { |
||||
|
isLoad.value = false |
||||
|
let edg = window.orientation == 0 || window.orientation == 180 ? 90 : 0 |
||||
|
let ratio = props.fullScreen ? edg : 0 |
||||
|
if (isImgaes(props.imgBack)) { |
||||
|
let res = await rotateBase64Img(props.imgBack, ratio, 'init') |
||||
|
if (res) { |
||||
|
imgBackDom.value = new Image(); |
||||
|
imgBackDom.value.crossOrigin = "anonymous" |
||||
|
imgBackDom!.value!.src = res; |
||||
|
} |
||||
|
} |
||||
|
if (isImgaes(props.canvasBack)){ |
||||
|
let res = await rotateBase64Img(props.canvasBack, ratio, 'init') |
||||
|
if (res) { |
||||
|
canvasBackDom.value = new Image(); |
||||
|
canvasBackDom.value.crossOrigin = "anonymous" |
||||
|
canvasBackDom!.value!.src = res; |
||||
|
canvasBackDom!.value.onload = () => { |
||||
|
isLoad.value = true |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
onMounted(() => { |
||||
|
// 根据当前屏幕转向获取需要裁减的宽度 |
||||
|
let directionWidth = window.orientation == 0 || window.orientation == 180 ? props.verticalDeductWidth : props.acrossDeductWidth |
||||
|
// 根据当前屏幕转向获取需要裁减的高度 |
||||
|
let directionHeight = window.orientation == 0 || window.orientation == 180 ? props.verticalDeductHeight : props.acrossDeductHeight |
||||
|
screenPatams.width = (document.body.clientWidth || document.body.offsetWidth) - directionWidth |
||||
|
screenPatams.height = (document.body.clientHeight || document.body.offsetHeight) - directionHeight |
||||
|
getImages() |
||||
|
getDomSize() |
||||
|
window.addEventListener("orientationchange", orientationchangeEvent) |
||||
|
resizeHandler(props.recoverPoints && props.recoverPoints.length && !props.isBrush ? true : false) |
||||
|
// 在画板以外松开鼠标后冻结画笔 |
||||
|
document.onmouseup= () => { |
||||
|
isDrawing.value = false |
||||
|
} |
||||
|
}) |
||||
|
const getDirection = () => { |
||||
|
return window.orientation == 90 || window.orientation == -90 ? 'across' : 'vertical' |
||||
|
} |
||||
|
// 毛笔新加---------------------------------------------------------------- |
||||
|
const distance = (a: {x:number, y:number}, b: {x:number, y:number}) => { |
||||
|
let x = b.x - a.x , y = b.y - a.y; |
||||
|
return Math.sqrt(x*x+y*y); |
||||
|
} |
||||
|
const customMouseDown = (e: {x:number, y:number, direction: string}) => { |
||||
|
hasPoints.value = [] |
||||
|
startX.value = e.x |
||||
|
startY.value = e.y |
||||
|
pointsArr.value.unshift(e); |
||||
|
} |
||||
|
const customMouseMove = (e: {x:number, y:number, direction: string}) => { |
||||
|
let of = e; //move |
||||
|
let up = { |
||||
|
x: startX.value, |
||||
|
y: startY.value, |
||||
|
} //down |
||||
|
hasPoints.value.unshift({time:new Date().getTime() ,dis: distance(up,of)}); |
||||
|
let dis = 0; |
||||
|
for (let n = 0; n < hasPoints.value.length-1; n++) { |
||||
|
dis += hasPoints.value[n].dis; |
||||
|
if (dis > smoothness.value) |
||||
|
break; |
||||
|
} |
||||
|
startX.value = of.x; |
||||
|
startY.value = of.y; |
||||
|
let len = Math.round(hasPoints.value[0].dis/2)+1; |
||||
|
for (let i = 0; i < len; i++) { |
||||
|
let x = up.x + (of.x-up.x)/len*i; |
||||
|
let y = up.y + (of.y-up.y)/len*i; |
||||
|
canvasTxt.value?.beginPath(); |
||||
|
|
||||
|
x = x-l.value /2; |
||||
|
y = y - l.value /2; |
||||
|
pointsArr.value.unshift({x,y,direction: e.direction}); |
||||
|
canvasTxt.value?.drawImage(penRef.value,x,y,l.value ,l.value ); |
||||
|
l.value = l.value - 0.2; |
||||
|
if( l.value < 10) l.value = 10; |
||||
|
} |
||||
|
} |
||||
|
const customMouseUp = () => { |
||||
|
l.value = props.brushLine < 20 ? 20 : props.brushLine; |
||||
|
if(pointsArr.value.length > 100){ |
||||
|
for(var j = 0; j <60 ;j++){ |
||||
|
pointsArr.value[j].x = pointsArr.value[j].x-l.value/4; |
||||
|
pointsArr.value[j].y = pointsArr.value[j].y - l.value/4; |
||||
|
canvasTxt.value?.drawImage(penRef.value,pointsArr.value[j].x,pointsArr.value[j].y,l.value,l.value); |
||||
|
|
||||
|
l.value = l.value - 0.3; |
||||
|
if( l.value < 5) l.value = 5; |
||||
|
} |
||||
|
l.value = props.brushLine < 20 ? 20 : props.brushLine; |
||||
|
pointsArr.value = []; |
||||
|
} |
||||
|
if (pointsArr.value.length==1) { |
||||
|
canvasTxt.value?.drawImage(penRef.value,pointsArr.value[0].x - l.value/2,pointsArr.value[0].y - l.value/2,l.value,l.value); |
||||
|
pointsArr.value = []; |
||||
|
} |
||||
|
} |
||||
|
// 毛笔新加---------------------------------------------------------------- |
||||
|
// pc |
||||
|
const onMouseDown = (e: any) => { |
||||
|
e = e || event |
||||
|
e.preventDefault() |
||||
|
isDrawing.value = true |
||||
|
hasDrew.value = true |
||||
|
let params = { |
||||
|
x: e.offsetX, |
||||
|
y: e.offsetY, |
||||
|
direction: getDirection() |
||||
|
} |
||||
|
// 毛笔新加--------------------------------- |
||||
|
if (props.isBrush) { |
||||
|
customMouseDown(params) |
||||
|
} else { |
||||
|
drawStart(params) |
||||
|
} |
||||
|
// 毛笔新加--------------------------------- |
||||
|
emits('onMouseDown', e) |
||||
|
} |
||||
|
const onMouseMove = (e: any) => { |
||||
|
e = e || event |
||||
|
e.preventDefault() |
||||
|
if (isDrawing.value) { |
||||
|
let obj = { |
||||
|
x: e.offsetX, |
||||
|
y: e.offsetY, |
||||
|
direction: getDirection() |
||||
|
} |
||||
|
// 毛笔新加--------------------------------- |
||||
|
if (props.isBrush) { |
||||
|
customMouseMove(obj) |
||||
|
} else { |
||||
|
drawMove(obj) |
||||
|
} |
||||
|
// 毛笔新加--------------------------------- |
||||
|
} |
||||
|
emits('onMouseMove', e) |
||||
|
} |
||||
|
const onMouseUp = (e: any) => { |
||||
|
e = e || event |
||||
|
e.preventDefault() |
||||
|
let obj = { |
||||
|
x: e.offsetX, |
||||
|
y: e.offsetY, |
||||
|
direction: getDirection() |
||||
|
} |
||||
|
if (props.isBrush) { |
||||
|
customMouseUp() |
||||
|
} else { |
||||
|
drawEnd(obj) |
||||
|
} |
||||
|
isDrawing.value = false |
||||
|
emits('onMouseUp', e) |
||||
|
} |
||||
|
// mobile |
||||
|
const onTouchStart = (e: any) => { |
||||
|
e = e || event |
||||
|
e.preventDefault() |
||||
|
hasDrew.value = true |
||||
|
if (e.touches.length === 1) { |
||||
|
let canvas = canvasRef.value as HTMLCanvasElement |
||||
|
let obj = { |
||||
|
x: e.targetTouches[0].clientX - canvas.getBoundingClientRect().left, |
||||
|
y: e.targetTouches[0].clientY - canvas.getBoundingClientRect().top, |
||||
|
direction: getDirection() |
||||
|
} |
||||
|
// 毛笔新加--------------------------------- |
||||
|
if (props.isBrush) { |
||||
|
customMouseDown(obj) |
||||
|
} else { |
||||
|
drawStart(obj) |
||||
|
} |
||||
|
// 毛笔新加--------------------------------- |
||||
|
} |
||||
|
emits('onTouchStart', e) |
||||
|
} |
||||
|
const onTouchMove = (e: any) => { |
||||
|
e = e || event |
||||
|
e.preventDefault() |
||||
|
if (e.touches.length >= 1) { |
||||
|
let canvas = canvasRef.value as HTMLCanvasElement |
||||
|
let obj = { |
||||
|
x: e.targetTouches[0].clientX - canvas.getBoundingClientRect().left, |
||||
|
y: e.targetTouches[0].clientY - canvas.getBoundingClientRect().top, |
||||
|
direction: getDirection() |
||||
|
} |
||||
|
// 毛笔新加--------------------------------- |
||||
|
if (props.isBrush) { |
||||
|
customMouseMove(obj) |
||||
|
} else { |
||||
|
drawMove(obj) |
||||
|
} |
||||
|
// 毛笔新加--------------------------------- |
||||
|
} |
||||
|
emits('onTouchMove', e) |
||||
|
} |
||||
|
const onTouchEnd = (e: any) => { |
||||
|
e = e || event |
||||
|
e.preventDefault() |
||||
|
console.log(e.touches, 'e.touches') |
||||
|
if (e.touches.length === 1) { |
||||
|
let canvas = canvasRef.value as HTMLCanvasElement |
||||
|
let obj = { |
||||
|
x: e.targetTouches[0].clientX - canvas.getBoundingClientRect().left, |
||||
|
y: e.targetTouches[0].clientY - canvas.getBoundingClientRect().top, |
||||
|
direction: getDirection() |
||||
|
} |
||||
|
// 毛笔新加--------------------------------- |
||||
|
if (props.isBrush) { |
||||
|
customMouseUp() |
||||
|
} else { |
||||
|
drawEnd(obj) |
||||
|
} |
||||
|
// 毛笔新加--------------------------------- |
||||
|
} else { |
||||
|
if (props.isBrush) { |
||||
|
customMouseUp() |
||||
|
} else { |
||||
|
points.value.push({x: -1, y: -1, direction: getDirection()}) |
||||
|
} |
||||
|
} |
||||
|
emits('onTouchEnd', e) |
||||
|
} |
||||
|
// 绘制 |
||||
|
const drawStart = (params: { x: number, y: number}) => { |
||||
|
startX.value = params.x |
||||
|
startY.value = params.y |
||||
|
canvasTxt.value?.beginPath() |
||||
|
canvasTxt.value?.moveTo(startX.value, startY.value) |
||||
|
canvasTxt.value?.lineTo(params.x, params.y) |
||||
|
canvasTxt!.value!.lineCap = 'round' |
||||
|
canvasTxt!.value!.lineJoin = 'round' |
||||
|
canvasTxt!.value!.lineWidth = props.lineWidth * sratio.value |
||||
|
canvasTxt.value?.stroke() |
||||
|
canvasTxt.value?.closePath() |
||||
|
points.value.push(params) |
||||
|
} |
||||
|
const drawMove = (params: { x: number, y: number}) => { |
||||
|
canvasTxt.value?.beginPath() |
||||
|
canvasTxt.value?.moveTo(startX.value, startY.value) |
||||
|
canvasTxt.value?.lineTo(params.x, params.y) |
||||
|
canvasTxt!.value!.strokeStyle = props.lineColor |
||||
|
canvasTxt!.value!.lineWidth = props.lineWidth * sratio.value |
||||
|
canvasTxt!.value!.lineCap = 'round' |
||||
|
canvasTxt!.value!.lineJoin = 'round' |
||||
|
canvasTxt.value?.stroke() |
||||
|
canvasTxt.value?.closePath() |
||||
|
startY.value = params.y |
||||
|
startX.value = params.x |
||||
|
points.value.push(params) |
||||
|
} |
||||
|
const drawEnd = (params: { x: number, y: number}) => { |
||||
|
canvasTxt.value?.beginPath() |
||||
|
canvasTxt.value?.moveTo(startX.value, startY.value) |
||||
|
canvasTxt.value?.lineTo(params.x, params.y) |
||||
|
canvasTxt!.value!.lineCap = 'round' |
||||
|
canvasTxt!.value!.lineJoin = 'round' |
||||
|
canvasTxt.value?.stroke() |
||||
|
canvasTxt.value?.closePath() |
||||
|
points.value.push(params) |
||||
|
points.value.push({x: -1, y: -1}) |
||||
|
} |
||||
|
const autoDraw = (canvasRefs: HTMLCanvasElement | null, canvas2d: CanvasRenderingContext2D | null) => { |
||||
|
if ((points.value && points.value.length || props.recoverPoints && props.recoverPoints.length) && !props.isBrush) { |
||||
|
let canvas = canvasRefs || canvasRef.value as HTMLCanvasElement |
||||
|
let canvasText = canvas2d || canvasTxt.value |
||||
|
let pointsList = props.recoverPoints && props.recoverPoints.length ? props.recoverPoints : points.value |
||||
|
if (pointsList && pointsList.length) { |
||||
|
hasDrew.value = true |
||||
|
} |
||||
|
pointsList.reduce((acc, cur) => { |
||||
|
if (cur.x != -1 && cur.y != -1 && acc.x != -1 && acc.y != -1) { |
||||
|
canvasText?.beginPath() |
||||
|
let position = { accX: acc.x, accY: acc.y, curX: cur.x, curY: cur.y } |
||||
|
if (props.fullScreen) { |
||||
|
if ((window.orientation == 0 || window.orientation == 180) && acc.direction == 'across' && cur.direction == 'across') { // 横屏笔画横屏转竖屏 |
||||
|
position = { accX: canvas.width - acc.y, accY: acc.x, curX: canvas.width - cur.y, curY: cur.x } |
||||
|
} else if ((window.orientation == 90 || window.orientation == -90) && acc.direction == 'vertical' && cur.direction == 'vertical') { // 竖屏笔画竖屏转横屏 |
||||
|
position = { accX: acc.y, accY: canvas.height - acc.x, curX: cur.y, curY: canvas.height - cur.x } |
||||
|
} |
||||
|
} |
||||
|
canvasText!.moveTo(position.accX, position.accY) |
||||
|
canvasText!.lineTo(position.curX, position.curY) |
||||
|
canvasText!.strokeStyle = props.lineColor |
||||
|
canvasText!.lineWidth = props.lineWidth * sratio.value |
||||
|
canvasText!.lineCap = 'round' |
||||
|
canvasText!.lineJoin = 'round' |
||||
|
canvasText!.stroke() |
||||
|
canvasText!.closePath() |
||||
|
} |
||||
|
return cur |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
// 操作 |
||||
|
const confirm = () => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
if (!hasDrew.value) { |
||||
|
reject(`Warning: Not Signned!`) |
||||
|
return |
||||
|
} |
||||
|
let canvas = canvasRef.value as HTMLCanvasElement |
||||
|
let resImgData = (canvasTxt.value as CanvasRenderingContext2D).getImageData(0, 0, canvas.width, canvas.height) |
||||
|
canvasTxt!.value!.globalCompositeOperation = "destination-over" |
||||
|
if (props.canvasBack && props.imgBack && !props.isBrush) { |
||||
|
canvasTxt.value?.clearRect(0,0,canvas.width ,canvas.height); |
||||
|
autoDraw(null, null) |
||||
|
} |
||||
|
// canvas背景图没有的情况下才生成图片背景图 |
||||
|
if (props.imgBack && isImgaes(props.imgBack) && !props.isBrush){ |
||||
|
let pat = canvasTxt.value?.createPattern(imgBackDom.value, (props.isRepeat || "no-repeat")); |
||||
|
canvasTxt.value?.rect(0,0,canvas.width ,canvas.height); |
||||
|
canvasTxt!.value!.fillStyle = (pat as any); |
||||
|
canvasTxt.value?.fill(); |
||||
|
} else if(props.imgBack && !isImgaes(props.imgBack) && !props.isBrush) { |
||||
|
canvasTxt!.value!.fillStyle = props.imgBack; |
||||
|
canvasTxt.value?.fillRect(0,0, canvas.width, canvas.height); |
||||
|
} |
||||
|
resultImg.value = canvas.toDataURL() |
||||
|
let resultImgs:any = resultImg.value |
||||
|
canvasTxt.value?.clearRect(0, 0, canvas.width ,canvas.height) |
||||
|
canvasTxt.value?.putImageData(resImgData, 0, 0) |
||||
|
canvasTxt!.value!.globalCompositeOperation = "source-over" |
||||
|
if (props.isCrop) { |
||||
|
const crop_area = getCropArea(resImgData.data) as [number, number, number,number] |
||||
|
let crop_canvas: HTMLCanvasElement | null = document.createElement('canvas') |
||||
|
const crop_ctx = crop_canvas.getContext('2d') |
||||
|
crop_canvas.width = crop_area[2] - crop_area[0] |
||||
|
crop_canvas.height = crop_area[3] - crop_area[1] |
||||
|
const crop_imgData = (cropCanvasTxt.value || canvasTxt.value)?.getImageData(...crop_area) |
||||
|
//const crop_imgData = (cropCanvasTxt.value || canvasTxt.value)?.getImageData.apply(null, crop_area) |
||||
|
crop_ctx!.globalCompositeOperation = "destination-over" |
||||
|
crop_ctx?.putImageData(crop_imgData!, 0, 0) |
||||
|
resultImgs = crop_canvas.toDataURL() |
||||
|
crop_canvas = null |
||||
|
} |
||||
|
let edg = (props.fullScreen && ((window.orientation == 0 || window.orientation == 180) || ((window.orientation == 90 || window.orientation == -90) && !props.noRotation))) || (window.orientation === undefined) && props.noRotation ? props.edg : 0 |
||||
|
rotateBase64Img(resultImgs, edg) |
||||
|
.then(base64 => { |
||||
|
resolve({ |
||||
|
base64, |
||||
|
points: points.value |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const reset = () => { |
||||
|
let canvas = canvasRef.value as HTMLCanvasElement |
||||
|
canvasTxt.value?.clearRect( |
||||
|
0, |
||||
|
0, |
||||
|
canvas.width, |
||||
|
canvas.height |
||||
|
) |
||||
|
points.value = [] |
||||
|
hasDrew.value = false |
||||
|
resultImg.value = '' |
||||
|
setCanvasBack(false) |
||||
|
} |
||||
|
const getCropArea = (imgData: any) => { |
||||
|
if (props.imgBack && !props.isBrush) { |
||||
|
cropCanvas.value = document.createElement('canvas') |
||||
|
if (props.domId) { |
||||
|
let dom = document.getElementById(props.domId) |
||||
|
let domWidth = dom ? dom.clientWidth || dom.offsetWidth : props.fullScreen ? screenPatams.width : props.width |
||||
|
let domHeight = dom ? dom.clientHeight || dom.offsetHeight : props.fullScreen ? screenPatams.height : props.height |
||||
|
cropCanvas.value.height = domHeight |
||||
|
cropCanvas.value.width = domWidth |
||||
|
domPatams.width = domWidth |
||||
|
domPatams.height = domHeight |
||||
|
} else { |
||||
|
cropCanvas.value.height = props.fullScreen ? screenPatams.height : props.height |
||||
|
cropCanvas.value.width = props.fullScreen ? screenPatams.width : props.width |
||||
|
} |
||||
|
cropCanvasTxt.value = cropCanvas.value.getContext('2d') |
||||
|
if (isImgaes(props.imgBack)){ |
||||
|
let pat = cropCanvasTxt.value?.createPattern(imgBackDom.value, (props.isRepeat || "no-repeat")); |
||||
|
cropCanvasTxt.value?.rect(0,0,cropCanvas.value.width ,cropCanvas.value.height); |
||||
|
cropCanvasTxt!.value!.fillStyle = (pat as any); |
||||
|
cropCanvasTxt.value?.fill(); |
||||
|
} else if(!isImgaes(props.imgBack)) { |
||||
|
cropCanvasTxt!.value!.fillStyle = props.imgBack; |
||||
|
cropCanvasTxt.value?.fillRect(0,0, cropCanvas.value.width, cropCanvas.value.height); |
||||
|
} |
||||
|
autoDraw(cropCanvas.value, cropCanvasTxt.value) |
||||
|
} |
||||
|
let canvas = cropCanvas.value as HTMLCanvasElement || canvasRef.value as HTMLCanvasElement |
||||
|
let topX = canvas.width; |
||||
|
var btmX = 0; |
||||
|
var topY = canvas.height; |
||||
|
var btnY = 0 |
||||
|
for (var i = 0; i < canvas.width; i++) { |
||||
|
for (var j = 0; j < canvas.height; j++) { |
||||
|
var pos = (i + canvas.width * j) * 4 |
||||
|
if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) { |
||||
|
btnY = Math.max(j, btnY) |
||||
|
btmX = Math.max(i, btmX) |
||||
|
topY = Math.min(j, topY) |
||||
|
topX = Math.min(i, topX) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
topX++ |
||||
|
btmX++ |
||||
|
topY++ |
||||
|
btnY++ |
||||
|
const data = [topX, topY, btmX, btnY] |
||||
|
return data |
||||
|
} |
||||
|
const recoverDraw = (pointsList: pointsType[]) => { |
||||
|
if (props.isBrush) return false |
||||
|
let canvas = canvasRef.value as HTMLCanvasElement |
||||
|
canvasTxt.value?.clearRect( |
||||
|
0, |
||||
|
0, |
||||
|
canvas.width, |
||||
|
canvas.height |
||||
|
) |
||||
|
points.value = pointsList |
||||
|
hasDrew.value = true |
||||
|
resultImg.value = '' |
||||
|
setCanvasBack(true) |
||||
|
} |
||||
|
onBeforeMount(() => { |
||||
|
if (props.fullScreen) { |
||||
|
// let bodyDom = document.getElementsByTagName('body') |
||||
|
// bodyDom[0].style = 'height: 100vh'; |
||||
|
document.body.style.height = '100vh'; |
||||
|
} |
||||
|
window.addEventListener('resize', resizeHandler) |
||||
|
}) |
||||
|
onUnmounted(() => { |
||||
|
if (props.fullScreen) { |
||||
|
// let bodyDom = document.getElementsByTagName('body') |
||||
|
// bodyDom[0].style = ''; |
||||
|
document.body.style.height = ''; |
||||
|
} |
||||
|
window.removeEventListener('resize', resizeHandler) |
||||
|
window.removeEventListener("orientationchange", orientationchangeEvent) |
||||
|
}) |
||||
|
defineExpose({ |
||||
|
confirm: toRaw(confirm), |
||||
|
reset, |
||||
|
recoverDraw |
||||
|
}) |
||||
|
|
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
canvas { |
||||
|
max-width: 100%; |
||||
|
display: block; |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue