Sign.vue 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. <template>
  2. <div class="sign">
  3. <vue-esign v-show="showSign" ref="esign" :disabled="disabled" />
  4. <div v-show="!showSign" class="history">
  5. <img :src="modelValue" alt="历史签名" />
  6. </div>
  7. <div v-if="!disabled" class="footer">
  8. <div v-if="state === 'default'" class="tip">
  9. <InfoCircleOutlined />
  10. <span>请在虚线框内签名</span>
  11. </div>
  12. <div v-else-if="state === 'error'" class="tip error">
  13. <ExclamationCircleOutlined />
  14. <span>签名上传失败,请尝试重新上传</span>
  15. </div>
  16. <div v-else-if="state === 'success'" class="tip success">
  17. <CheckOutlined />
  18. <span>已签名,若有问题可以点击“重签”重新签名</span>
  19. </div>
  20. <div>
  21. <a-button size="small" shape="round" @click="clear">重签</a-button>
  22. <a-button type="primary" shape="round" size="small" @click="confirm">确认</a-button>
  23. </div>
  24. </div>
  25. </div>
  26. </template>
  27. <script setup lang="ts">
  28. import { onMounted, ref, type PropType } from 'vue';
  29. import VueEsign from 'vue-esign';
  30. import { InfoCircleOutlined, ExclamationCircleOutlined, CheckOutlined } from '@ant-design/icons-vue';
  31. import { Form, message, Modal } from 'ant-design-vue';
  32. import type { UploadCoInterface } from './UploadImageFormItem';
  33. const props = defineProps({
  34. disabled: {
  35. type: Boolean,
  36. default: false
  37. },
  38. modelValue: {
  39. type: String,
  40. default: ''
  41. },
  42. /**
  43. * 上传工厂类
  44. */
  45. uploadCo: {
  46. type: Object as PropType<UploadCoInterface>,
  47. default: null,
  48. },
  49. });
  50. const emit = defineEmits(['update:modelValue']);
  51. const esign = ref();
  52. const state = ref<'default'|'error'|'success'>('default');
  53. const showSign = ref(true);
  54. const formItemContext = Form.useInjectFormItemContext();
  55. onMounted(() => {
  56. if (props.modelValue) {
  57. state.value = 'success';
  58. showSign.value = false;
  59. }
  60. })
  61. const clear = () => {
  62. if (props.modelValue) {
  63. Modal.confirm({
  64. title: '确认清除签名?',
  65. okText: '清除',
  66. onOk: () => {
  67. emit('update:modelValue', '');
  68. formItemContext.onFieldChange();
  69. esign.value.reset();
  70. state.value = 'default';
  71. showSign.value = true;
  72. }
  73. })
  74. } else {
  75. state.value = 'default';
  76. esign.value.reset();
  77. showSign.value = true;
  78. }
  79. }
  80. const confirm = () => {
  81. esign.value.generate().then((res: string) => {
  82. if (props.uploadCo) {
  83. //上传
  84. return new Promise((resolve, reject) => {
  85. const blob = base64ToBlob(res, 'image/png');
  86. const file = new File([blob], 'image.png', { type: 'image/png' });
  87. props.uploadCo.uploadRequest({
  88. file: file,
  89. filename: 'sign.png',
  90. action: '',
  91. headers: {},
  92. withCredentials: true,
  93. method: 'post',
  94. data: {},
  95. onProgress: () => {},
  96. onSuccess: (res) => {
  97. resolve(res.url);
  98. },
  99. onError: (err) => {
  100. reject(err);
  101. },
  102. })
  103. }).then((res) => {
  104. message.success('签名上传成功');
  105. state.value = 'success';
  106. formItemContext.onFieldChange();
  107. showSign.value = false;
  108. emit('update:modelValue', res);
  109. }).catch((err) => {
  110. state.value = 'error';
  111. Modal.error({
  112. title: '上传失败',
  113. content: '签名上传失败,请尝试重新上传:' + err.message,
  114. })
  115. });
  116. } else {
  117. // 不上传,直接返回base64字符串
  118. formItemContext.onFieldChange();
  119. showSign.value = false;
  120. emit('update:modelValue', res);
  121. }
  122. });
  123. }
  124. function base64ToBlob(base64: string, mimeType = 'image/png') {
  125. const byteCharacters = atob(base64.split(',')[1]); // 去掉 data:image/png;base64, 前缀
  126. const byteNumbers = new Array(byteCharacters.length);
  127. for (let i = 0; i < byteCharacters.length; i++) {
  128. byteNumbers[i] = byteCharacters.charCodeAt(i);
  129. }
  130. const byteArray = new Uint8Array(byteNumbers);
  131. return new Blob([byteArray], { type: mimeType });
  132. }
  133. </script>
  134. <style lang="scss" scoped>
  135. .sign {
  136. width: 100%;
  137. height: 100%;
  138. border: 1px dashed #ddd;
  139. border-radius: 10px;
  140. overflow: hidden;
  141. .history {
  142. position: relative;
  143. margin-bottom: 10px;
  144. img {
  145. max-width: 100%;
  146. height: 100%;
  147. object-fit: contain;
  148. }
  149. }
  150. .tip {
  151. font-size: 12px;
  152. color: #999;
  153. &.error {
  154. color: #bd2028;
  155. }
  156. &.success {
  157. color: #198754;
  158. }
  159. }
  160. .footer {
  161. padding: 10px;
  162. display: flex;
  163. flex-direction: row;
  164. align-items: center;
  165. justify-content: space-between;
  166. > div {
  167. display: flex;
  168. gap: 5px;
  169. }
  170. }
  171. }
  172. </style>