Explorar el Código

📦 专家审核增加签名

快乐的梦鱼 hace 1 mes
padre
commit
af8a7e8aff

+ 2 - 0
env.d.ts

@@ -1 +1,3 @@
 /// <reference types="vite/client" />
+
+declare module 'vue-esign';

+ 59 - 0
package-lock.json

@@ -29,6 +29,7 @@
         "tslib": "^2.8.1",
         "vue": "^3.5.18",
         "vue-clipboard3": "^2.0.0",
+        "vue-esign": "^1.1.4",
         "vue-router": "^4.5.1",
         "vue3-carousel": "^0.15.0"
       },
@@ -11362,6 +11363,22 @@
         "node": ">=18"
       }
     },
+    "node_modules/prettier": {
+      "version": "2.8.8",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+      "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+      "license": "MIT",
+      "optional": true,
+      "bin": {
+        "prettier": "bin-prettier.js"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
     "node_modules/pretty-bytes": {
       "version": "6.1.1",
       "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
@@ -14227,6 +14244,48 @@
       "integrity": "sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==",
       "license": "MIT"
     },
+    "node_modules/vue-esign": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/vue-esign/-/vue-esign-1.1.4.tgz",
+      "integrity": "sha512-7Ix5PdcyyhVfsvrT9a+yp5+36gbQ0/bpDO+QSLT58IgJ5t164PEptOy5Nslw8bZbk3n3Hc7SP5B8eXQ8X8W+OA==",
+      "license": "MIT",
+      "dependencies": {
+        "vue": "^2.5.11"
+      }
+    },
+    "node_modules/vue-esign/node_modules/@vue/compiler-sfc": {
+      "version": "2.7.16",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz",
+      "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==",
+      "dependencies": {
+        "@babel/parser": "^7.23.5",
+        "postcss": "^8.4.14",
+        "source-map": "^0.6.1"
+      },
+      "optionalDependencies": {
+        "prettier": "^1.18.2 || ^2.0.0"
+      }
+    },
+    "node_modules/vue-esign/node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/vue-esign/node_modules/vue": {
+      "version": "2.7.16",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz",
+      "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==",
+      "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-sfc": "2.7.16",
+        "csstype": "^3.1.0"
+      }
+    },
     "node_modules/vue-router": {
       "version": "4.5.1",
       "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",

+ 1 - 0
package.json

@@ -36,6 +36,7 @@
     "tslib": "^2.8.1",
     "vue": "^3.5.18",
     "vue-clipboard3": "^2.0.0",
+    "vue-esign": "^1.1.4",
     "vue-router": "^4.5.1",
     "vue3-carousel": "^0.15.0"
   },

+ 0 - 3
src/common/upload/AliOssUploadCo.ts

@@ -11,9 +11,6 @@ const client = new OSS({
 
 export function useAliOssUploadCo(subPath: string) : UploadCoInterface {
   return {
-    requestUploadToken: async (key: string,  bucketNameDa: string, expire ?:number) => {
-      return '';
-    },
     uploadRequest: async (requestOption: AntUploadRequestOption) => {
       const uploadPath = `${subPath}/${requestOption.file.name.split('.')[0]}-${RandomUtils.genNonDuplicateID(26)}.${StringUtils.path.getFileExt(requestOption.file.name)}`;      
 

+ 1 - 4
src/common/upload/ImageUploadCo.ts

@@ -4,11 +4,8 @@ import type { AntUploadRequestOption, UploadCoInterface } from "@/components/dyn
 export function useImageSimpleUploadCo(additionData?: Record<string, any>) : UploadCoInterface {
 
   return {
-    requestUploadToken: async (key: string,  bucketNameDa: string, expire ?:number) => {
-      return '';
-    },
     uploadRequest: (requestOption: AntUploadRequestOption) => {
-      CommonContent.uploadSmallFile(requestOption.file, 'image', requestOption.filename, additionData)
+      CommonContent.uploadSmallFile(requestOption.file, 'image', 'file', additionData)
         .then((res) => {
           requestOption.onSuccess?.({
             url: res.fullurl,

+ 178 - 0
src/components/dynamicf/Sign.vue

@@ -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>
+
+

+ 0 - 4
src/components/dynamicf/UploadImageFormItem.ts

@@ -46,10 +46,6 @@ export interface UploadImageFormItemProps {
  */
 export interface UploadCoInterface {
   /**
-   * 请求上传Token
-   */
-  requestUploadToken : (key: string,  bucketNameDa: string, expire ?:number) => Promise<string>,
-  /**
     * 上传主函数。由 ant-upload 调用。
     */
   uploadRequest: (requestOption: AntUploadRequestOption) => void,

+ 1 - 1
src/components/dynamicf/UploadImageFormItem.vue

@@ -163,7 +163,7 @@ function handleBeforeUpload(file: FileItem) {
     needRemoveItem.push(file.uid);
   return result;
 }
-function handleUpload(requestOption: AntUploadRequestOption) {
+async function handleUpload(requestOption: AntUploadRequestOption) {
   props.uploadCo?.uploadRequest(requestOption);
 }
 function handleUploadSubImgReject(e: FileInfo) {

+ 2 - 0
src/components/dynamicf/index.ts

@@ -31,6 +31,7 @@ import { QuillEditor } from "@vueup/vue-quill";
 import QuillEditorWrapper from "./Editor/QuillEditorWrapper.vue";
 import UploadVideoFormItem from "./UploadVideoFormItem.vue";
 import AddressSercher from "./Map/AddressSercher.vue";
+import Sign from "./Sign.vue";
 
 export const defaultConfig = {
   internalWidgets: {
@@ -98,5 +99,6 @@ export function registerAllFormComponents() {
     .register('map-pick-point', markRaw(MapPointPicker), {}, 'modelValue')
     .register('richtext', markRaw(QuillEditorWrapper), {}, 'modelValue')
     .register('address-sercher', markRaw(AddressSercher), {}, 'modelValue')
+    .register('sign', markRaw(Sign), {}, 'modelValue')
 
 }

+ 1 - 1
src/pages/admin.vue

@@ -233,7 +233,7 @@ async function loadSeminarData(page: number, pageSize: number, dropDownValues: n
     });
     return {
       page,
-      total: res.length,
+      total: pageSize,
       data: res.map((item) => ({
         ...item,
         desc: item.address,

+ 13 - 1
src/pages/forms/form.vue

@@ -134,6 +134,7 @@ import type { DataModel } from '@imengyu/js-request-transform';
 import InheritorContent, { InheritorWorkInfo } from '@/api/inheritor/InheritorContent';
 import CommonListBlock from '@/components/content/CommonListBlock.vue';
 import { waitTimeOut } from '@imengyu/imengyu-utils';
+import { useImageSimpleUploadCo } from '@/common/upload/ImageUploadCo';
 
 const isMobile = computed(() => {
   return window.innerWidth < 768;
@@ -280,10 +281,21 @@ const finalFormOptions = computed(() => {
                 placeholder: { callback: (_: any, model: any) => (isAdmin.value || isReviewer.value) ? '若审核不通过,请输入审核意见' : '暂无审核意见' },
               }
             },
+            {
+              label: '审核签名', name: 'sign', type: 'sign',
+              hidden: { callback: (_: any, model: any) => !isReviewer.value },
+              additionalProps: {
+                uploadCo: useImageSimpleUploadCo({}),
+              }
+            },
           ]
         }
       ] : [])
     ],
+    formRules: {
+      ...formOptions.value.formRules,
+      sign: [{ required: true, message: '请审核签名', trigger: ['blur'] }],
+    },
     disabled: readonly.value,
   }
 });
@@ -295,7 +307,7 @@ async function handleSubmitBase(valid: boolean) {
   loading.value = true;
 
   if (valid) {
-    if (!await new Promise((resolve, reject) => {
+    if (!isAdmin.value && !await new Promise((resolve, reject) => {
       Modal.confirm({
         title: '提交提示',
         content: '是否提交信息审核?填写完整信息后才可提交审核。如果需要离开,可先保存修改下次接着编辑。',