Bladeren bron

feat: 添加占位签到码

wyf 9 maanden geleden
bovenliggende
commit
39f060387a

+ 6 - 13
src/api/emergency-drill/emergency-drill.ts

@@ -7,6 +7,7 @@ import {
   DrillPlanItemDetail,
   DrillPlanRecord,
   ExecuteDrillPlanRuleForm,
+  DrillRecordRuleForm,
 } from '@/views/emergency/emergency-drill/types';
 import type { DrillSignlistQuery, DrillSignListResponse } from '@/types/emergency-drill';
 
@@ -67,31 +68,23 @@ export const submitEmergencyDrillExecute = (params: ExecuteDrillPlanRuleForm) =>
   });
 };
 
-export const saveEmergencyDrillRecord = (params) => {
+export const saveEmergencyDrillRecord = (params: DrillRecordRuleForm) => {
   return http.request({
     url: '/emergencyDrill/saveEmergencyDrillRecord',
     method: 'POST',
     data: {
-      drillPlanId: params.drillPlanId,
-      // participants: params.participants,
-      drillDescription: params.drillDescription,
-      drillImage: params.drillImage,
-      drillEffectAssess: params.drillEffectAssess,
+      ...params,
       saveOrSubmit: 0,
     },
   });
 };
 
-export const submitEmergencyDrillRecord = (params) => {
+export const submitEmergencyDrillRecord = (params: DrillRecordRuleForm) => {
   return http.request({
     url: '/emergencyDrill/saveEmergencyDrillRecord',
     method: 'POST',
     data: {
-      drillPlanId: params.drillPlanId,
-      // participants: params.participants,
-      drillDescription: params.drillDescription,
-      drillImage: params.drillImage,
-      drillEffectAssess: params.drillEffectAssess,
+      ...params,
       saveOrSubmit: 1,
     },
   });
@@ -159,7 +152,7 @@ export const signDrillScript = (data: { drillPlanId: number; planToParticipateCo
  */
 export const queryDrillApproval = (id: number) => {
   return http.request<any>({
-    url: `/emergencyDrill/queryApprovalProcess?drillPlanId=${id}`,
+    url: `/approvalManagement/queryApprovalProcess?drillPlanId=${id}`,
     method: 'get',
   });
 };

+ 5 - 0
src/components/Qrcode/index.ts

@@ -0,0 +1,5 @@
+import { withInstall } from '@/utils';
+import qrCode from './src/Qrcode.vue';
+
+export const QrCode = withInstall(qrCode);
+export * from './src/typing';

+ 114 - 0
src/components/Qrcode/src/Qrcode.vue

@@ -0,0 +1,114 @@
+<template>
+  <div>
+    <component :is="tag" ref="wrapRef" />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, watch, PropType, ref, unref, onMounted } from 'vue';
+  import { toCanvas, QRCodeRenderersOptions, LogoType } from './qrcodePlus';
+  import { toDataURL } from 'qrcode';
+  import { downloadByUrl } from '@/utils/file/download';
+  import { QrcodeDoneEventParams } from './typing';
+
+  export default defineComponent({
+    name: 'QrCode',
+    props: {
+      value: {
+        type: [String, Array] as PropType<string | any[]>,
+        default: null,
+      },
+      // 参数
+      options: {
+        type: Object as PropType<QRCodeRenderersOptions>,
+        default: null,
+      },
+      // 宽度
+      width: {
+        type: Number as PropType<number>,
+        default: 200,
+      },
+      // 中间logo图标
+      logo: {
+        type: [String, Object] as PropType<Partial<LogoType> | string>,
+        default: '',
+      },
+      // img 不支持内嵌logo
+      tag: {
+        type: String as PropType<'canvas' | 'img'>,
+        default: 'canvas',
+        validator: (v: string) => ['canvas', 'img'].includes(v),
+      },
+    },
+    emits: { done: (data: QrcodeDoneEventParams) => !!data, error: (error: any) => !!error },
+    setup(props, { emit }) {
+      const wrapRef = ref<HTMLCanvasElement | HTMLImageElement | null>(null);
+
+      async function createQrcode() {
+        try {
+          const { tag, value, options = {}, width, logo } = props;
+          const renderValue = String(value);
+          const wrapEl = unref(wrapRef);
+
+          if (!wrapEl) return;
+
+          if (tag === 'canvas') {
+            const url: string = await toCanvas({
+              canvas: wrapEl,
+              width,
+              logo: logo as any,
+              content: renderValue,
+              options: options || {},
+            });
+            emit('done', { url, ctx: (wrapEl as HTMLCanvasElement).getContext('2d') });
+            return;
+          }
+
+          if (tag === 'img') {
+            const url = await toDataURL(renderValue, {
+              errorCorrectionLevel: 'H',
+              width,
+              ...options,
+            });
+            (unref(wrapRef) as HTMLImageElement).src = url;
+            emit('done', { url });
+          }
+        } catch (error) {
+          emit('error', error);
+        }
+      }
+
+      /**
+       * file download
+       */
+      function download(fileName?: string) {
+        let url = '';
+        const wrapEl = unref(wrapRef);
+        if (wrapEl instanceof HTMLCanvasElement) {
+          url = wrapEl.toDataURL();
+        } else if (wrapEl instanceof HTMLImageElement) {
+          url = wrapEl.src;
+        }
+        if (!url) return;
+        downloadByUrl({
+          url,
+          fileName,
+        });
+      }
+
+      onMounted(createQrcode);
+
+      // 监听参数变化重新生成二维码
+      watch(
+        props,
+        () => {
+          createQrcode();
+        },
+        {
+          deep: true,
+        },
+      );
+
+      return { wrapRef, download };
+    },
+  });
+</script>

+ 37 - 0
src/components/Qrcode/src/drawCanvas.ts

@@ -0,0 +1,37 @@
+import { toCanvas } from 'qrcode';
+import type { QRCodeRenderersOptions } from 'qrcode';
+import { RenderQrCodeParams, ContentType } from './typing';
+import { cloneDeep } from 'lodash-es';
+
+export const renderQrCode = ({
+  canvas,
+  content,
+  width = 0,
+  options: params = {},
+}: RenderQrCodeParams) => {
+  const options = cloneDeep(params);
+  // 容错率,默认对内容少的二维码采用高容错率,内容多的二维码采用低容错率
+  options.errorCorrectionLevel = options.errorCorrectionLevel || getErrorCorrectionLevel(content);
+
+  return getOriginWidth(content, options).then((_width: number) => {
+    options.scale = width === 0 ? undefined : (width / _width) * 4;
+    return toCanvas(canvas, content, options);
+  });
+};
+
+// 得到原QrCode的大小,以便缩放得到正确的QrCode大小
+function getOriginWidth(content: ContentType, options: QRCodeRenderersOptions) {
+  const _canvas = document.createElement('canvas');
+  return toCanvas(_canvas, content, options).then(() => _canvas.width);
+}
+
+// 对于内容少的QrCode,增大容错率
+function getErrorCorrectionLevel(content: ContentType) {
+  if (content.length > 36) {
+    return 'M';
+  } else if (content.length > 16) {
+    return 'Q';
+  } else {
+    return 'H';
+  }
+}

+ 88 - 0
src/components/Qrcode/src/drawLogo.ts

@@ -0,0 +1,88 @@
+import { isString } from '@/utils/is';
+import { RenderQrCodeParams, LogoType } from './typing';
+export const drawLogo = ({ canvas, logo }: RenderQrCodeParams) => {
+  if (!logo) {
+    return new Promise((resolve) => {
+      resolve((canvas as HTMLCanvasElement).toDataURL());
+    });
+  }
+  const canvasWidth = (canvas as HTMLCanvasElement).width;
+  const {
+    logoSize = 0.15,
+    bgColor = '#ffffff',
+    borderSize = 0.05,
+    crossOrigin,
+    borderRadius = 8,
+    logoRadius = 0,
+  } = logo as LogoType;
+
+  const logoSrc: string = isString(logo) ? logo : logo.src;
+  const logoWidth = canvasWidth * logoSize;
+  const logoXY = (canvasWidth * (1 - logoSize)) / 2;
+  const logoBgWidth = canvasWidth * (logoSize + borderSize);
+  const logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2;
+
+  const ctx = canvas.getContext('2d');
+  if (!ctx) return;
+
+  // logo 底色
+  canvasRoundRect(ctx)(logoBgXY, logoBgXY, logoBgWidth, logoBgWidth, borderRadius);
+  ctx.fillStyle = bgColor;
+  ctx.fill();
+
+  // logo
+  const image = new Image();
+  if (crossOrigin || logoRadius) {
+    image.setAttribute('crossOrigin', crossOrigin || 'anonymous');
+  }
+  image.src = logoSrc;
+
+  // 使用image绘制可以避免某些跨域情况
+  const drawLogoWithImage = (image: CanvasImageSource) => {
+    ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
+  };
+
+  // 使用canvas绘制以获得更多的功能
+  const drawLogoWithCanvas = (image: HTMLImageElement) => {
+    const canvasImage = document.createElement('canvas');
+    canvasImage.width = logoXY + logoWidth;
+    canvasImage.height = logoXY + logoWidth;
+    const imageCanvas = canvasImage.getContext('2d');
+    if (!imageCanvas || !ctx) return;
+    imageCanvas.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
+
+    canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius);
+    if (!ctx) return;
+    const fillStyle = ctx.createPattern(canvasImage, 'no-repeat');
+    if (fillStyle) {
+      ctx.fillStyle = fillStyle;
+      ctx.fill();
+    }
+  };
+
+  // 将 logo绘制到 canvas上
+  return new Promise((resolve) => {
+    image.onload = () => {
+      logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image);
+      resolve((canvas as HTMLCanvasElement).toDataURL());
+    };
+  });
+};
+
+// copy来的方法,用于绘制圆角
+function canvasRoundRect(ctx: CanvasRenderingContext2D) {
+  return (x: number, y: number, w: number, h: number, r: number) => {
+    const minSize = Math.min(w, h);
+    if (r > minSize / 2) {
+      r = minSize / 2;
+    }
+    ctx.beginPath();
+    ctx.moveTo(x + r, y);
+    ctx.arcTo(x + w, y, x + w, y + h, r);
+    ctx.arcTo(x + w, y + h, x, y + h, r);
+    ctx.arcTo(x, y + h, x, y, r);
+    ctx.arcTo(x, y, x + w, y, r);
+    ctx.closePath();
+    return ctx;
+  };
+}

+ 4 - 0
src/components/Qrcode/src/qrcodePlus.ts

@@ -0,0 +1,4 @@
+// 参考 qr-code-with-logo 进行ts版本修改
+import { toCanvas } from './toCanvas';
+export * from './typing';
+export { toCanvas };

+ 10 - 0
src/components/Qrcode/src/toCanvas.ts

@@ -0,0 +1,10 @@
+import { renderQrCode } from './drawCanvas';
+import { drawLogo } from './drawLogo';
+import { RenderQrCodeParams } from './typing';
+export const toCanvas = (options: RenderQrCodeParams) => {
+  return renderQrCode(options)
+    .then(() => {
+      return options;
+    })
+    .then(drawLogo) as Promise<string>;
+};

+ 39 - 0
src/components/Qrcode/src/typing.ts

@@ -0,0 +1,39 @@
+import type { QRCodeSegment, QRCodeRenderersOptions } from 'qrcode';
+import { Fn } from '/#/index';
+
+export type ContentType = string | QRCodeSegment[];
+
+export type { QRCodeRenderersOptions };
+
+export type LogoType = {
+  src: string;
+  logoSize: number;
+  borderColor: string;
+  bgColor: string;
+  borderSize: number;
+  crossOrigin: string;
+  borderRadius: number;
+  logoRadius: number;
+};
+
+export interface RenderQrCodeParams {
+  canvas: any;
+  content: ContentType;
+  width?: number;
+  options?: QRCodeRenderersOptions;
+  logo?: LogoType | string;
+  image?: HTMLImageElement;
+  downloadName?: string;
+  download?: boolean | Fn;
+}
+
+export type ToCanvasFn = (options: RenderQrCodeParams) => Promise<unknown>;
+
+export interface QrCodeActionType {
+  download: (fileName?: string) => void;
+}
+
+export interface QrcodeDoneEventParams {
+  url: string;
+  ctx?: CanvasRenderingContext2D | null;
+}

+ 17 - 3
src/views/emergency/emergency-drill/components/DrillPlanViewActivities.vue

@@ -15,7 +15,13 @@
       <div class="data">
         <div v-for="item in DRILL_VIEW_EXECUTE" :key="item.value" class="item">
           <span class="label">{{ item.label }}</span>
-          <a v-if="item.link" class="link" :href="drillData![item.link]">{{ drillData![item.value] }}</a>
+          <el-popover v-if="item.popover" placement="bottom" trigger="hover" width="224">
+            <template #reference>
+              <span style="cursor: pointer">查看签到码</span>
+            </template>
+            <QrCode :value="drillData![item.popover]" :width="200" />
+          </el-popover>
+          <a v-else-if="item.link" class="link" :href="drillData![item.link]">{{ drillData![item.value] }}</a>
           <span v-else class="value"> {{ drillData![item.value] }}</span>
         </div>
       </div>
@@ -24,6 +30,9 @@
       <div class="name">演练参与部门</div>
       <el-button style="margin-top: 20px" type="primary" :icon="Download" @click=""> 下载签到名单 </el-button>
       <BasicTable style="margin-top: 20px" :tableConfig="tableConfig" :tableData="drillData.planDetailList!">
+        <template #drillScriptStatus="scope">
+          <span>{{ scope.row.drillScriptStatus === 1 ? '未会签' : '已会签' }}</span>
+        </template>
       </BasicTable>
     </div>
   </div>
@@ -31,6 +40,7 @@
 
 <script setup lang="ts">
   import { onMounted, ref } from 'vue';
+  import { ElPopover } from 'element-plus';
   import BasicTable from '@/components/BasicTable.vue';
   import { Download } from '@element-plus/icons-vue';
   import { useRoute } from 'vue-router';
@@ -41,6 +51,7 @@
   import { queryEmergencyDrillPlanDetail, queryEmergencyPlanDetail } from '@/api/emergency-drill/emergency-drill';
   import { DRILL_PLAN_ACTIVITIES_TABLE_OPTIONS, DRILL_PLAN_ACTIVITIES_TABLE_COLUMNS } from '../configs/plan/table';
   import { useEmergencyDrillHook } from '../hook';
+  import QrCode from '@/components/Qrcode/src/Qrcode.vue';
 
   const route = useRoute();
   const id = route.query.id;
@@ -56,7 +67,7 @@
       // 解析详情数据
       drillData.value.drillScope = drillScopeDice.value.find(
         (x) => x.itemCode === drillData.value!.drillScope,
-      )?.itemValue;
+      )!.itemValue;
 
       drillData.value.responsibleDeptNameList = drillData.value.responsibleDeptNameList!.replace(/^\[|\]$/g, '');
       drillData.value.coordinateDeptNameList = drillData.value.coordinateDeptNameList
@@ -75,10 +86,13 @@
 
       if (drillData.value.drillScript && drillData.value.drillScript.length > 0) {
         const scriptFile = unformatAttachment(drillData.value.drillScript);
-        console.log(scriptFile);
+        // console.log(scriptFile);
         drillData.value.drillScriptName = scriptFile.fileName;
         drillData.value.drillScriptUrl = scriptFile.fileUrl;
       }
+
+      drillData.value.qrCode = 'https://cn.bing.com/?id=' + id + '&type=test';
+
       tableConfig.loading = false;
     } catch (e) {
       console.log(e);

+ 1 - 1
src/views/emergency/emergency-drill/configs/plan/table.ts

@@ -12,7 +12,7 @@ export const DRILL_PLAN_LIST_TABLE_COLUMNS: TableColumnProps[] = [
     label: '序号',
     align: 'center',
     width: '80px',
-    type: 'index'
+    type: 'index',
   },
   {
     prop: 'drillScope',

+ 3 - 3
src/views/emergency/emergency-drill/constants.ts

@@ -95,8 +95,8 @@ export const DRILL_VIEW_EXECUTE = [
   },
   {
     label: '签到码:',
-    value: 'qr',
-    link: 'qr',
+    value: 'qrCode',
+    popover: 'qrCode',
   },
 ];
 
@@ -156,4 +156,4 @@ export const CONFIRM_STATUS_MAP = {
   [CONFIRM_STATUS_TYPE.WAIT_CONFIRM]: '待确认',
   [CONFIRM_STATUS_TYPE.CONFIRMED]: '已确认',
   [CONFIRM_STATUS_TYPE.REJECT]: '已退回',
-};
+};

+ 5 - 0
src/views/emergency/emergency-drill/types.ts

@@ -133,6 +133,8 @@ export interface DrillPlanItemDetail {
     /*0-未删除,大于0(时间戳)-已删除 */
     isDeleted?: number;
   }[];
+  /*签到码*/
+  qrCode?: string;
 }
 
 export interface ExecuteDrillPlanRuleForm {
@@ -204,6 +206,9 @@ export interface DrillRecordRuleForm {
   /*演练效果评估 */
   drillEffectAssess?: string;
   /*演练记录审批内容 */
+  approvalDescription?: string;
+  /*审批人 */
+  approvalInfoList?: { approvalOrder?: number; approverIdList?: string }[];
 }
 
 // export interface DrillApprovalItem {