Toggle navigation
Toggle navigation
This project
Loading...
Sign in
Hooke
/
data-table
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Graphs
Network
Create a new issue
Commits
Issue Boards
Authored by
hookehuyr
2023-03-30 11:07:50 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
2e21190cec40ecd02d3ebd5fbee3ef8e879973cf
2e21190c
1 parent
8067ff93
✨ feat(电子签名控件): 自定义表单钩子函数调整
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
299 additions
and
281 deletions
components.d.ts
src/components/SignField/MyComponent.vue
src/components/SignField/index.vue
src/views/index.vue
components.d.ts
View file @
2e21190
...
...
@@ -19,7 +19,7 @@ declare module '@vue/runtime-core' {
CalendarField
:
typeof
import
(
'./src/components/CalendarField/index.vue'
)[
'default'
]
CheckboxField
:
typeof
import
(
'./src/components/CheckboxField/index.vue'
)[
'default'
]
ContactField
:
typeof
import
(
'./src/components/ContactField/index.vue'
)[
'default'
]
copy
:
typeof
import
(
'./src/components/
Des
Field copy/index.vue'
)[
'default'
]
copy
:
typeof
import
(
'./src/components/
Sign
Field copy/index.vue'
)[
'default'
]
CustomField
:
typeof
import
(
'./src/components/CustomField/index.vue'
)[
'default'
]
DatePickerField
:
typeof
import
(
'./src/components/DatePickerField/index.vue'
)[
'default'
]
DateTimePickerField
:
typeof
import
(
'./src/components/DateTimePickerField/index.vue'
)[
'default'
]
...
...
@@ -32,7 +32,8 @@ declare module '@vue/runtime-core' {
ImageUploaderField
:
typeof
import
(
'./src/components/ImageUploaderField/index.vue'
)[
'default'
]
MarqueeField
:
typeof
import
(
'./src/components/MarqueeField/index.vue'
)[
'default'
]
MultiRuleField
:
typeof
import
(
'./src/components/MultiRuleField/index.vue'
)[
'default'
]
MyComponent
:
typeof
import
(
'./src/components/CustomField/MyComponent.vue'
)[
'default'
]
MyComponent
:
typeof
import
(
'./src/components/AppointmentField/MyComponent.vue'
)[
'default'
]
MyComponet
:
typeof
import
(
'./src/components/SignField/MyComponet.vue'
)[
'default'
]
NameField
:
typeof
import
(
'./src/components/NameField/index.vue'
)[
'default'
]
NoteField
:
typeof
import
(
'./src/components/NoteField/index.vue'
)[
'default'
]
NumberField
:
typeof
import
(
'./src/components/NumberField/index.vue'
)[
'default'
]
...
...
@@ -44,6 +45,7 @@ declare module '@vue/runtime-core' {
RouterView
:
typeof
import
(
'vue-router'
)[
'RouterView'
]
RuleField
:
typeof
import
(
'./src/components/RuleField/index.vue'
)[
'default'
]
SignField
:
typeof
import
(
'./src/components/SignField/index.vue'
)[
'default'
]
'SignField copy'
:
typeof
import
(
'./src/components/SignField copy/index.vue'
)[
'default'
]
TableField
:
typeof
import
(
'./src/components/TableField/index.vue'
)[
'default'
]
Test
:
typeof
import
(
'./src/components/VideoField/test.vue'
)[
'default'
]
TextareaField
:
typeof
import
(
'./src/components/TextareaField/index.vue'
)[
'default'
]
...
...
src/components/SignField/MyComponent.vue
0 → 100644
View file @
2e21190
<!--
* @Date: 2023-03-29 17:44:24
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-03-30 11:04:24
* @FilePath: /data-table/src/components/SignField/MyComponent.vue
* @Description: 文件描述
-->
<template>
<div style="width: 100%;">
<div ref="wrapperRef" class="esign-wrapper">
<vue-esign v-if="esignWidth" ref="esign" class="sign-wrapper" :width="esignWidth" :height="esignHeight" :isCrop="isCrop"
:lineWidth="lineWidth" :lineColor="lineColor" :bgColor.sync="bgColor" />
<div v-if="show_sign" class="whiteboard">
<div class="text" @click="startSign">
<van-icon name="edit" /> 点击开始签署电子签名
</div>
</div>
</div>
<div v-if="!show_sign">
<div v-if="show_control" class="control-sign">
<van-row gutter="20" style="padding: 0 1rem">
<van-col :span="12">
<van-button type="default" block @click="handleGenerate">确认签名</van-button>
</van-col>
<van-col :span="12">
<van-button type="default" block @click="cancelSign">取消签名</van-button>
</van-col>
</van-row>
</div>
<div v-else style="padding: 0 1rem">
<van-button type="danger" block @click="handleReset">删除签名</van-button>
</div>
</div>
<van-divider />
</div>
<van-overlay :show="loading">
<div class="wrapper" @click.stop>
<van-loading vertical color="#FFFFFF">生成中...</van-loading>
</div>
</van-overlay>
</template>
<script setup>
import { inject, ref } from 'vue'
import { useCustomFieldValue } from '@vant/use';
import { v4 as uuidv4 } from "uuid";
import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from "@/api/common";
import { showSuccessToast, showFailToast } from "vant";
import { useRoute } from "vue-router";
import BMF from "browser-md5-file";
import { getEtag } from "@/utils/qetag.js"; // 生成hash值
import dayjs from "dayjs";
// 获取父组件传值
const props = inject('props');
const $route = useRoute();
const esign = ref(null);
let esignWidth = ref();
let esignHeight = ref();
const wrapperRef = ref(null)
onMounted(() => {
// 动态计算画板canvas宽度/高度
setTimeout(() => {
esignWidth.value = wrapperRef.value.offsetWidth - 32;
esignHeight.value = (window.innerHeight) / 5;
}, 100);
})
const lineWidth = ref(6);
const lineColor = ref("#000000");
const bgColor = ref("#FCFCFC");
const isCrop = ref(false);
const show_control = ref(true);
const image_url = ref("");
const show_empty = ref(false);
const sign_value = computed(() => {
return { name: `电子签名${dayjs().format('YYYYMMDDHHmmss')}.png`, url: image_url.value }
})
const handleReset = () => {
// 清空画板
esign.value.reset();
show_control.value = true;
// 删除可能存在的签名
image_url.value = "";
};
/********** 上传七牛云获取图片地址 ***********/
const loading = ref(false);
const formCode = $route.query.code; // 表单code
const uuid = () => {
let s = [];
let hexDigits = "0123456789abcdef";
for (var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = "4";
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
s[8] = s[13] = s[18] = s[23] = "-";
var uuid = s.join("");
return uuid;
};
const uploadQiniu = async (file, token, filename) => {
let formData = new FormData();
formData.append("file", file); // 通过append向form对象添加数据
formData.append("token", token);
formData.append("key", filename);
let config = {
headers: { "Content-Type": "multipart/form-data" },
};
// 自拍图片上传七牛服务器
let qiniuUploadUrl;
if (window.location.protocol === 'https:') {
qiniuUploadUrl = 'https://up.qbox.me';
} else {
qiniuUploadUrl = 'http://upload.qiniu.com';
}
const { filekey, hash, image_info } = await qiniuUploadAPI(
qiniuUploadUrl,
formData,
config
);
if (filekey) {
// 保存图片
const { data } = await saveFileAPI({
filekey,
hash,
format: image_info.format,
height: image_info.height,
width: image_info.width,
});
return data;
}
};
/****************** END *******************/
const handleUpload = async (files, filename) => {
// 上传图片流程
loading.value = true;
// 获取HASH值
const hash = getEtag(files);
// 获取七牛token
const { token, key, code } = await qiniuTokenAPI({
name: filename,
hash,
});
// 文件上传七牛云
const imgUrl = await uploadQiniu(files, token, filename);
return imgUrl;
};
const handleGenerate = () => {
esign.value
.generate()
.then(async (res) => {
let affix = uuidv4();
let fileName = `uploadForm/${formCode}/${affix}_sign.png`;
let file = dataURLtoFile(res, fileName); // 生成文件
const imgUrl = await handleUpload(file, fileName);
loading.value = false;
image_url.value = imgUrl.src;
show_control.value = false;
show_empty.value = false;
})
.catch((err) => {
loading.value = false;
// 签名生成失败
console.warn(err);
if (err) {
showFailToast("签名生成失败");
}
});
};
//将图片base64转换为文件
const dataURLtoFile = (dataurl, filename) => {
var arr = dataurl.split(","),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mime });
};
const show_sign = ref(true);
const startSign = () => {
show_sign.value = false;
show_empty.value = false;
};
const cancelSign = () => {
show_sign.value = true;
show_empty.value = false;
handleReset();
};
// 此处传入的值会替代 Field 组件内部的 value
useCustomFieldValue(() => sign_value.value);
</script>
<style lang="less" scoped>
.esign-wrapper {
padding: 1rem;
position: relative;
box-sizing: border-box;
.sign-wrapper {
border: 1px solid #eaeaea;
border-radius: 5px;
background: #fcfcfc !important;
}
.whiteboard {
position: absolute;
height: 100%;
width: 100%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
text-align: center;
.text {
position: absolute;
width: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
.control-sign {
padding-bottom: 1rem;
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.block {
width: 120px;
height: 120px;
background-color: #fff;
}
</style>
src/components/SignField/index.vue
View file @
2e21190
<!--
* @Date: 2022-09-06 16:29:31
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-0
2-10 15:31:06
* @LastEditTime: 2023-0
3-29 17:58:28
* @FilePath: /data-table/src/components/SignField/index.vue
* @Description: 电子签名控件
-->
...
...
@@ -10,241 +10,46 @@
<div class="label">
<span v-if="item.component_props.required"> *</span>
{{ item.component_props.label }}
{{ valid }}
</div>
<div ref="wrapperRef" class="esign-wrapper">
<!-- <div style="padding: 1rem; position: relative; height: 150px; background-color: #FCFCFC;border: 1px solid #EAEAEA; border-radius: 5px;"> -->
<vue-esign v-if="esignWidth" ref="esign" class="sign-wrapper" :width="esignWidth" :height="esignHeight" :isCrop="isCrop"
:lineWidth="lineWidth" :lineColor="lineColor" :bgColor.sync="bgColor" />
<div v-if="show_sign" class="whiteboard">
<div class="text" @click="startSign">
<van-icon name="edit" /> 点击开始签署电子签名
<van-field :name="item.key" :rules="rules" style="padding: 0;">
<template #input>
<my-component />
</template>
</van-field>
</div>
</div>
</div>
<div v-if="!show_sign">
<div v-if="show_control" class="control-sign">
<van-row gutter="20" style="padding: 0 1rem">
<van-col :span="12">
<van-button type="default" block @click="handleGenerate">确认签名</van-button>
</van-col>
<van-col :span="12">
<van-button type="default" block @click="cancelSign">取消签名</van-button>
</van-col>
</van-row>
</div>
<div v-else style="padding: 0 1rem">
<van-button type="danger" block @click="handleReset">删除签名</van-button>
</div>
</div>
<div v-if="show_empty" class="van-field__error-message" style="padding: 0 1rem 1rem 1rem">
电子签名不能为空
</div>
<van-divider />
</div>
<van-overlay :show="loading">
<div class="wrapper" @click.stop>
<van-loading vertical color="#FFFFFF">生成中...</van-loading>
</div>
</van-overlay>
</template>
<script setup>
import { v4 as uuidv4 } from "uuid";
import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from "@/api/common";
import { showSuccessToast, showFailToast } from "vant";
import { useRoute } from "vue-router";
import BMF from "browser-md5-file";
import { getEtag } from "@/utils/qetag.js"; // 生成hash值
import dayjs from "dayjs";
import MyComponent from './MyComponent.vue';
const props = defineProps({
item: Object,
});
// 注入子组件属性
provide('props', props.item);
// 隐藏显示
const HideShow = computed(() => {
return !props.item.component_props.disabled
})
const $route = useRoute();
const emit = defineEmits(["active"]);
const esign = ref(null);
let esignWidth = ref();
let esignHeight = ref();
const wrapperRef = ref(null)
onMounted(() => {
// 动态计算画板canvas宽度/高度
setTimeout(() => {
esignWidth.value = wrapperRef.value.offsetWidth - 32;
esignHeight.value = (window.innerHeight) / 5;
}, 100);
})
const lineWidth = ref(6);
const lineColor = ref("#000000");
const bgColor = ref("#FCFCFC");
const isCrop = ref(false);
const show_control = ref(true);
const image_url = ref("");
const show_empty = ref(false);
const handleReset = () => {
// 清空画板
esign.value.reset();
show_control.value = true;
// 删除可能存在的签名
image_url.value = "";
props.item.value = {
key: "sign",
filed_name: props.item.key,
value: "",
};
emit("active", props.item.value);
};
/********** 上传七牛云获取图片地址 ***********/
const loading = ref(false);
const formCode = $route.query.code; // 表单code
const uuid = () => {
let s = [];
let hexDigits = "0123456789abcdef";
for (var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = "4";
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1);
s[8] = s[13] = s[18] = s[23] = "-";
var uuid = s.join("");
return uuid;
};
const uploadQiniu = async (file, token, filename) => {
let formData = new FormData();
formData.append("file", file); // 通过append向form对象添加数据
formData.append("token", token);
formData.append("key", filename);
let config = {
headers: { "Content-Type": "multipart/form-data" },
};
// 自拍图片上传七牛服务器
let qiniuUploadUrl;
if (window.location.protocol === 'https:') {
qiniuUploadUrl = 'https://up.qbox.me';
// 规则校验
const required = props.item.component_props.required;
const validator = (val) => {
if (required && !val.url) {
return false;
} else {
qiniuUploadUrl = 'http://upload.qiniu.com';
}
const { filekey, hash, image_info } = await qiniuUploadAPI(
qiniuUploadUrl,
formData,
config
);
if (filekey) {
// 保存图片
const { data } = await saveFileAPI({
filekey,
hash,
format: image_info.format,
height: image_info.height,
width: image_info.width,
});
return data;
}
};
/****************** END *******************/
const handleUpload = async (files, filename) => {
// 上传图片流程
loading.value = true;
// 获取HASH值
const hash = getEtag(files);
// 获取七牛token
const { token, key, code } = await qiniuTokenAPI({
name: filename,
hash,
});
// 文件上传七牛云
const imgUrl = await uploadQiniu(files, token, filename);
return imgUrl;
};
const handleGenerate = () => {
esign.value
.generate()
.then(async (res) => {
let affix = uuidv4();
let fileName = `uploadForm/${formCode}/${affix}_sign.png`;
let file = dataURLtoFile(res, fileName); // 生成文件
const imgUrl = await handleUpload(file, fileName);
loading.value = false;
props.item.value = {
key: "sign",
filed_name: props.item.key,
value: { name: `电子签名${dayjs().format('YYYYMMDDHHmmss')}.png`, url: imgUrl.src},
};
image_url.value = imgUrl.src;
show_control.value = false;
show_empty.value = false;
emit("active", props.item.value);
})
.catch((err) => {
loading.value = false;
// 签名生成失败
console.warn(err);
if (err) {
showFailToast("签名生成失败");
}
});
};
//将图片base64转换为文件
const dataURLtoFile = (dataurl, filename) => {
var arr = dataurl.split(","),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
return true;
}
return new File([u8arr], filename, { type: mime });
};
const show_sign = ref(true);
const startSign = () => {
show_sign.value = false;
show_empty.value = false;
};
const cancelSign = () => {
show_sign.value = true;
show_empty.value = false;
handleReset();
};
const validSign = () => {
// 必填项 未生成签名
if (props.item.component_props.required && !image_url.value) {
show_empty.value = true;
} else {
show_empty.value = false;
// 错误提示文案
const validatorMessage = (val, rule) => {
if (required && !val.url) {
return "电子签名不能为空";
}
return !show_empty.value;
};
defineExpose({ validSign });
const rules = [{ validator, message: validatorMessage }];
</script>
<!-- <script>
export default {
methods: {
validSign () {
console.warn(0);
}
}
}
</script> -->
<style lang="less" scoped>
.sign-page {
...
...
@@ -258,52 +63,9 @@ export default {
color: red;
}
}
.esign-wrapper {
padding: 1rem;
position: relative;
box-sizing: border-box;
.sign-wrapper {
border: 1px solid #eaeaea;
border-radius: 5px;
}
.whiteboard {
position: absolute;
height: 100%;
width: 100%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
text-align: center;
.text {
position: absolute;
width: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
}
.control-sign {
padding-bottom: 1rem;
}
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.block {
width: 120px;
height: 120px;
background-color: #fff;
:deep(.van-field__error-message) {
padding-left: 1rem;
}
</style>
...
...
src/views/index.vue
View file @
2e21190
<!--
* @Date: 2022-07-18 10:22:22
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2023-03-29 1
6:24:48
* @LastEditTime: 2023-03-29 1
7:57:51
* @FilePath: /data-table/src/views/index.vue
* @Description: 首页
-->
...
...
@@ -169,9 +169,9 @@ const setRefMap = (el, item) => {
if (item.component_props.tag === "file_uploader") {
file_uploader.value.push(el);
}
if (item.component_props.tag === "sign") {
sign.value.push(el);
}
//
if (item.component_props.tag === "sign") {
//
sign.value.push(el);
//
}
}
};
...
...
@@ -428,9 +428,9 @@ const onActive = (item) => {
if (item.key === "file_uploader") {
postData.value[item.filed_name] = item.value;
}
if (item.key === "sign") {
postData.value[item.filed_name] = item.value;
}
//
if (item.key === "sign") {
//
postData.value[item.filed_name] = item.value;
//
}
if (item.type === "radio") { // 单选控件
postData.value = _.assign(postData.value, { [item.key]: item.affix ? item.affix : item.value });
}
...
...
@@ -479,18 +479,18 @@ const validOther = () => {
}
});
}
if (sign.value) {
// 电子签名
sign.value.forEach((item, index) => {
if (!sign.value[index].validSign()) {
valid = {
status: sign.value[index].validSign(),
key: "sign",
};
return false;
}
});
}
//
if (sign.value) {
//
// 电子签名
//
sign.value.forEach((item, index) => {
//
if (!sign.value[index].validSign()) {
//
valid = {
//
status: sign.value[index].validSign(),
//
key: "sign",
//
};
//
return false;
//
}
//
});
//
}
return valid;
};
...
...
Please
register
or
login
to post a comment