|
|
@@ -0,0 +1,178 @@
|
|
|
+<template>
|
|
|
+ <div class="sign">
|
|
|
+ <vue-esign ref="esign" :disabled="disabled" />
|
|
|
+ <div v-if="showHistory" class="history">
|
|
|
+ <img :src="modelValue" alt="历史签名" />
|
|
|
+ </div>
|
|
|
+ <div v-if="!disabled" class="footer">
|
|
|
+ <div v-if="state === 'default'" class="tip">
|
|
|
+ <InfoCircleOutlined />
|
|
|
+ <span>请在虚线框内签名</span>
|
|
|
+ </div>
|
|
|
+ <div v-else-if="state === 'error'" class="tip error">
|
|
|
+ <ExclamationCircleOutlined />
|
|
|
+ <span>签名上传失败,请尝试重新上传</span>
|
|
|
+ </div>
|
|
|
+ <div v-else-if="state === 'success'" class="tip success">
|
|
|
+ <CheckOutlined />
|
|
|
+ <span>签名上传成功</span>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <a-button size="small" shape="round" @click="clear">重签</a-button>
|
|
|
+ <a-button type="primary" shape="round" size="small" @click="confirm">确认</a-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { onMounted, ref, type PropType } from 'vue';
|
|
|
+import VueEsign from 'vue-esign';
|
|
|
+import { InfoCircleOutlined, ExclamationCircleOutlined, CheckOutlined } from '@ant-design/icons-vue';
|
|
|
+import { Form, message, Modal } from 'ant-design-vue';
|
|
|
+import type { UploadCoInterface } from './UploadImageFormItem';
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ disabled: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ modelValue: {
|
|
|
+ type: String,
|
|
|
+ default: ''
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 上传工厂类
|
|
|
+ */
|
|
|
+ uploadCo: {
|
|
|
+ type: Object as PropType<UploadCoInterface>,
|
|
|
+ default: null,
|
|
|
+ },
|
|
|
+ showHistory: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+const emit = defineEmits(['update:modelValue']);
|
|
|
+const esign = ref();
|
|
|
+const state = ref<'default'|'error'|'success'>('default');
|
|
|
+
|
|
|
+const formItemContext = Form.useInjectFormItemContext();
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ if (props.modelValue) {
|
|
|
+ state.value = 'success';
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const clear = () => {
|
|
|
+ if (props.modelValue) {
|
|
|
+ Modal.confirm({
|
|
|
+ title: '确认清除签名?',
|
|
|
+ okText: '清除',
|
|
|
+ cancelText: '取消',
|
|
|
+ onOk: () => {
|
|
|
+ emit('update:modelValue', '');
|
|
|
+ formItemContext.onFieldChange();
|
|
|
+ esign.value.reset();
|
|
|
+ state.value = 'default';
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ state.value = 'default';
|
|
|
+ esign.value.reset();
|
|
|
+ }
|
|
|
+}
|
|
|
+const confirm = () => {
|
|
|
+ esign.value.generate().then((res: string) => {
|
|
|
+ if (props.uploadCo) {
|
|
|
+ //上传
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const blob = base64ToBlob(res, 'image/png');
|
|
|
+ const file = new File([blob], 'image.png', { type: 'image/png' });
|
|
|
+ props.uploadCo.uploadRequest({
|
|
|
+ file: file,
|
|
|
+ filename: 'sign.png',
|
|
|
+ action: '',
|
|
|
+ headers: {},
|
|
|
+ withCredentials: true,
|
|
|
+ method: 'post',
|
|
|
+ data: {},
|
|
|
+ onProgress: () => {},
|
|
|
+ onSuccess: (res) => {
|
|
|
+ resolve(res.url);
|
|
|
+ },
|
|
|
+ onError: (err) => {
|
|
|
+ reject(err);
|
|
|
+ },
|
|
|
+ })
|
|
|
+ }).then((res) => {
|
|
|
+ message.success('签名上传成功');
|
|
|
+ state.value = 'success';
|
|
|
+ formItemContext.onFieldChange();
|
|
|
+ emit('update:modelValue', res);
|
|
|
+ }).catch((err) => {
|
|
|
+ state.value = 'error';
|
|
|
+ Modal.error({
|
|
|
+ title: '上传失败',
|
|
|
+ content: '签名上传失败,请尝试重新上传:' + err.message,
|
|
|
+ })
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 不上传,直接返回base64字符串
|
|
|
+ formItemContext.onFieldChange();
|
|
|
+ emit('update:modelValue', res);
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function base64ToBlob(base64: string, mimeType = 'image/png') {
|
|
|
+ const byteCharacters = atob(base64.split(',')[1]); // 去掉 data:image/png;base64, 前缀
|
|
|
+ const byteNumbers = new Array(byteCharacters.length);
|
|
|
+ for (let i = 0; i < byteCharacters.length; i++) {
|
|
|
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
|
+ }
|
|
|
+ const byteArray = new Uint8Array(byteNumbers);
|
|
|
+ return new Blob([byteArray], { type: mimeType });
|
|
|
+}
|
|
|
+
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+
|
|
|
+.sign {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ border: 1px dashed #ddd;
|
|
|
+ border-radius: 10px;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ .tip {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #999;
|
|
|
+
|
|
|
+ &.error {
|
|
|
+ color: #bd2028;
|
|
|
+ }
|
|
|
+ &.success {
|
|
|
+ color: #198754;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .footer {
|
|
|
+ padding: 10px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+
|
|
|
+ > div {
|
|
|
+ display: flex;
|
|
|
+ gap: 5px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+</style>
|
|
|
+
|
|
|
+
|