hookehuyr

✨ feat: 电子签名功能完善

1 <!-- 1 <!--
2 * @Date: 2022-09-06 16:29:31 2 * @Date: 2022-09-06 16:29:31
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2022-09-07 00:46:48 4 + * @LastEditTime: 2022-09-07 15:58:45
5 * @FilePath: /data-table/src/components/SignField/index.vue 5 * @FilePath: /data-table/src/components/SignField/index.vue
6 - * @Description: 文件描述 6 + * @Description: 电子签名控件
7 --> 7 -->
8 <template> 8 <template>
9 <div class="sign-page"> 9 <div class="sign-page">
10 <div class="label">{{ item.label }}<span v-if="item.required">&nbsp;*</span></div> 10 <div class="label">{{ item.label }}<span v-if="item.required">&nbsp;*</span></div>
11 - <div style="padding: 1rem;"> 11 + <div style="padding: 1rem; position: relative;">
12 - 12 + <!-- <div style="padding: 1rem; position: relative; height: 150px; background-color: #FCFCFC;border: 1px solid #EAEAEA; border-radius: 5px;"> -->
13 - <vue-esign ref="esign" style="border: 1px dashed #c2c1c1;" :height="700" :isCrop="isCrop" :lineWidth="lineWidth" 13 + <vue-esign ref="esign" class="sign-wrapper" style="" :isCrop="isCrop" :lineWidth="lineWidth"
14 :lineColor="lineColor" :bgColor.sync="bgColor" /> 14 :lineColor="lineColor" :bgColor.sync="bgColor" />
15 + <div v-if="show_sign" class="whiteboard">
16 + <div class="text" @click="startSign">
17 + <van-icon name="edit" />&nbsp;点击开始签署电子签名
18 + </div>
19 + </div>
15 </div> 20 </div>
16 - <div class="control-sign"> 21 + <div v-if="!show_sign">
17 - <button @click="handleReset">清空画板</button> 22 + <div v-if="show_control" class="control-sign">
18 - <button @click="handleGenerate">生成图片</button> 23 + <van-row gutter="20" style="padding: 0 1rem;">
24 + <van-col :span="12">
25 + <van-button type="default" block @click="handleGenerate">确认签名</van-button>
26 + </van-col>
27 + <van-col :span="12">
28 + <van-button type="default" block @click="cancelSign">取消签名</van-button>
29 + </van-col>
30 + </van-row>
31 + </div>
32 + <div v-else style="padding: 0 1rem;">
33 + <van-button type="danger" block @click="handleReset">删除签名</van-button>
34 + </div>
19 </div> 35 </div>
20 </div> 36 </div>
21 </template> 37 </template>
22 38
23 <script setup> 39 <script setup>
40 +import { v4 as uuidv4 } from 'uuid';
41 +import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common'
42 +import { showSuccessToast, showFailToast } from 'vant';
43 +
24 const props = defineProps({ 44 const props = defineProps({
25 item: Object 45 item: Object
26 }); 46 });
27 -</script>
28 47
29 -<script> 48 +const emit = defineEmits(['active']);
30 -import { v4 as uuidv4 } from 'uuid';
31 -import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common'
32 49
33 -export default { 50 +const esign = ref(null);
34 - data() { 51 +
35 - return { 52 +const lineWidth = ref(6)
36 - lineWidth: 6, 53 +const lineColor = ref('#000000')
37 - lineColor: "#000000", 54 +const bgColor = ref('#FCFCFC')
38 - bgColor: "", 55 +const isCrop = ref(false)
39 - resultImg: "", 56 +const show_control = ref(true)
40 - isCrop: false, 57 +
41 - } 58 +const handleReset = () => {
42 - }, 59 + // 清空画板
43 - methods: { 60 + esign.value.reset();
44 - //清空画板.. 61 + show_control.value = true;
45 - handleReset() { 62 + // 删除可能存在的签名
46 - this.$refs.esign.reset(); 63 + props.item.value = { key: 'sign', value: '' };
47 - this.resultImg = ""; 64 + emit('active', props.item.value)
48 - }, 65 +}
49 - //生成签名图片.. 66 +
50 - handleGenerate () { 67 +const handleGenerate = () => {
51 - this.$refs.esign.generate() 68 + esign.value.generate()
52 - .then(async res => { 69 + .then(async res => {
53 - // let fileName = "img1.png"; 70 + // let fileName = "img1.png";
54 - // let file = this.dataURLtoFile(res, fileName); 71 + // let file = this.dataURLtoFile(res, fileName);
55 - // console.log("file", file); 72 + // console.log("file", file);
56 - let affix = uuidv4(); 73 + let affix = uuidv4();
57 - let base64url = res.slice(res.indexOf(',') + 1); // 截取前缀的base64 data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAnoAAAJeCAYAA....... 74 + let base64url = res.slice(res.indexOf(',') + 1); // 截取前缀的base64 data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAnoAAAJeCAYAA.......
58 - // 获取七牛token 75 + // 获取七牛token
59 - const { token, key, code } = await qiniuTokenAPI({ filename: `${affix}_sign`, file: base64url }); 76 + const { token, key, code } = await qiniuTokenAPI({ filename: `${affix}_sign`, file: base64url });
60 - if (code) { 77 + if (code) {
61 - const config = { 78 + const config = {
62 - headers: { 79 + headers: {
63 - 'Content-Type': 'application/octet-stream', 80 + 'Content-Type': 'application/octet-stream',
64 - 'Authorization': 'UpToken ' + token, // UpToken后必须有一个 ' '(空格) 81 + 'Authorization': 'UpToken ' + token, // UpToken后必须有一个 ' '(空格)
65 - } 82 + }
66 - } 83 + }
67 - // 上传七牛服务器 84 + // 上传七牛服务器
68 - const { filekey, hash, image_info } = await qiniuUploadAPI('http://upload.qiniup.com/putb64/-1/key/' + key, base64url, config) 85 + const { filekey, hash, image_info } = await qiniuUploadAPI('http://upload.qiniup.com/putb64/-1/key/' + key, base64url, config)
69 - if (filekey) { 86 + if (filekey) {
70 - // 保存图片 87 + // 保存图片
71 - const { data } = await saveFileAPI({ filekey, hash, format: image_info.format, height: image_info.height, width: image_info.width }); 88 + const { data } = await saveFileAPI({ filekey, hash, format: image_info.format, height: image_info.height, width: image_info.width });
72 - console.warn(data.src); 89 + props.item.value = { key: 'sign', value: data.src};
73 - } 90 + show_control.value = false;
74 - } 91 + emit('active', props.item.value)
75 - });
76 - },
77 - //将图片base64转换为文件
78 - dataURLtoFile(dataurl, filename) {
79 - var arr = dataurl.split(","),
80 - mime = arr[0].match(/:(.*?);/)[1],
81 - bstr = atob(arr[1]),
82 - n = bstr.length,
83 - u8arr = new Uint8Array(n);
84 - while (n--) {
85 - u8arr[n] = bstr.charCodeAt(n);
86 } 92 }
87 - return new File([u8arr], filename, { type: mime });
88 } 93 }
94 + })
95 + .catch(err => {
96 + // 签名生成失败
97 + console.warn(err);
98 + if (err) {
99 + showFailToast('签名生成失败');
100 + }
101 + })
102 +}
103 +
104 +//将图片base64转换为文件
105 +const dataURLtoFile = (dataurl, filename) => {
106 + var arr = dataurl.split(","),
107 + mime = arr[0].match(/:(.*?);/)[1],
108 + bstr = atob(arr[1]),
109 + n = bstr.length,
110 + u8arr = new Uint8Array(n);
111 + while (n--) {
112 + u8arr[n] = bstr.charCodeAt(n);
89 } 113 }
114 + return new File([u8arr], filename, { type: mime });
115 +}
116 +
117 +const show_sign = ref(true);
118 +const startSign = () => {
119 + show_sign.value = false;
120 +}
121 +const cancelSign = () => {
122 + show_sign.value = true;
123 + handleReset()
90 } 124 }
91 </script> 125 </script>
92 126
93 <style lang="less" scoped> 127 <style lang="less" scoped>
94 .sign-page { 128 .sign-page {
129 + padding-bottom: 1rem;
95 .label { 130 .label {
96 padding: 1rem 1rem 0 1rem; 131 padding: 1rem 1rem 0 1rem;
97 font-size: 0.9rem; 132 font-size: 0.9rem;
...@@ -101,5 +136,25 @@ export default { ...@@ -101,5 +136,25 @@ export default {
101 color: red; 136 color: red;
102 } 137 }
103 } 138 }
139 + .sign-wrapper {
140 + border: 1px solid #EAEAEA;
141 + border-radius: 5px;
142 + }
143 + .whiteboard {
144 + position: absolute;
145 + height: 100%;
146 + width: 100%;
147 + left: 50%;
148 + top: 50%;
149 + transform: translate(-50%, -50%);
150 + text-align: center;
151 + .text {
152 + position: absolute;
153 + width: 100%;
154 + top: 50%;
155 + left: 50%;
156 + transform: translate(-50%, -50%);
157 + }
158 + }
104 } 159 }
105 </style> 160 </style>
......
1 <!-- 1 <!--
2 * @Date: 2022-07-18 10:22:22 2 * @Date: 2022-07-18 10:22:22
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2022-09-07 14:12:37 4 + * @LastEditTime: 2022-09-07 16:02:34
5 * @FilePath: /data-table/src/views/index.vue 5 * @FilePath: /data-table/src/views/index.vue
6 * @Description: 首页 6 * @Description: 首页
7 --> 7 -->
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
11 <div class="table-box"> 11 <div class="table-box">
12 <van-form @submit="onSubmit"> 12 <van-form @submit="onSubmit">
13 <van-cell-group> 13 <van-cell-group>
14 - <component v-for="(item, index) in mockData" :key="index" :is="item.component" :item="item" /> 14 + <component v-for="(item, index) in mockData" :key="index" :is="item.component" :item="item" @active="onActive" />
15 </van-cell-group> 15 </van-cell-group>
16 <div style="margin: 16px;"> 16 <div style="margin: 16px;">
17 <van-button round block type="primary" native-type="submit"> 17 <van-button round block type="primary" native-type="submit">
...@@ -24,37 +24,39 @@ ...@@ -24,37 +24,39 @@
24 24
25 <script setup> 25 <script setup>
26 import { createComponentType } from '@/hooks/useComponentType' 26 import { createComponentType } from '@/hooks/useComponentType'
27 +import _ from 'lodash'
27 28
28 const table_cover = ref(''); 29 const table_cover = ref('');
29 const table_title = ref(''); 30 const table_title = ref('');
30 const mockData = ref([]); 31 const mockData = ref([]);
32 +const postData = ref({})
31 33
32 onMounted(() => { 34 onMounted(() => {
33 table_cover.value = 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg' 35 table_cover.value = 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg'
34 table_title.value = '这是一个表单的描述' 36 table_title.value = '这是一个表单的描述'
35 mockData.value = [ 37 mockData.value = [
36 - // { 38 + {
37 - // key: 'phone', 39 + key: 'phone',
38 - // value: '', 40 + value: '',
39 - // label: '手机号', 41 + label: '手机号',
40 - // placeholder: '请输入手机号', 42 + placeholder: '请输入手机号',
41 - // component: '', 43 + component: '',
42 - // component_props: { 44 + component_props: {
43 - // name: 'phone' 45 + name: 'phone'
44 - // }, 46 + },
45 - // required: true, 47 + required: true,
46 - // }, 48 + },
47 - // { 49 + {
48 - // key: 'username', 50 + key: 'username',
49 - // value: 'test', 51 + value: 'test',
50 - // label: '用户名', 52 + label: '用户名',
51 - // placeholder: '请输入用户名', 53 + placeholder: '请输入用户名',
52 - // component: '', 54 + component: '',
53 - // component_props: { 55 + component_props: {
54 - // name: 'text' 56 + name: 'text'
55 - // }, 57 + },
56 - // required: true, 58 + required: true,
57 - // }, 59 + },
58 // { 60 // {
59 // key: 'email', 61 // key: 'email',
60 // value: '', 62 // value: '',
...@@ -143,7 +145,7 @@ onMounted(() => { ...@@ -143,7 +145,7 @@ onMounted(() => {
143 placeholder: '', 145 placeholder: '',
144 component: '', 146 component: '',
145 component_props: { 147 component_props: {
146 - name: 'sign' 148 + name: 'sign',
147 }, 149 },
148 }, 150 },
149 // { 151 // {
...@@ -185,9 +187,17 @@ onMounted(() => { ...@@ -185,9 +187,17 @@ onMounted(() => {
185 }) 187 })
186 188
187 const onSubmit = (values) => { 189 const onSubmit = (values) => {
188 - console.log('submit', values); 190 + // 合并自定义字段到提交表单字段
191 + postData.value = _.assign(postData.value, values);
192 + console.warn(postData.value);
189 // console.warn(mockData.value); 193 // console.warn(mockData.value);
190 }; 194 };
195 +const onActive = (item) => {
196 + // 返回自定义字段
197 + if (item.key === 'sign') {
198 + postData.value['sign'] = item.value
199 + }
200 +}
191 </script> 201 </script>
192 202
193 <style lang="less" scoped> 203 <style lang="less" scoped>
......