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