riku
2025-06-24 4fbdf4c6b13d19b9be54900b5dcff29e2ca7ef01
巡查单据自动下载功能(待完成)
已修改9个文件
已添加3个文件
413 ■■■■■ 文件已修改
public/工地巡查单据模板.docx 补丁 | 查看 | 原始文档 | blame | 历史
public/餐饮巡查单据模板.doc 补丁 | 查看 | 原始文档 | blame | 历史
src/components.d.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/FYImageSelectDialog.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/doc.js 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/check/ProCheckProxy.js 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/check/components/ArbitraryPhoto.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/check/components/CompSubTaskStatistic.vue 40 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/scene/SceneInspectFile.vue 252 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/task/TaskManage.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/task/components/CompMonitorObjEdit.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fysp/task/components/CompSubTaskList.vue 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/¹¤µØÑ²²éµ¥¾ÝÄ£°å.docx
Binary files differ
public/²ÍÒûѲ²éµ¥¾ÝÄ£°å.doc
Binary files differ
src/components.d.ts
@@ -25,6 +25,7 @@
    ElCard: typeof import('element-plus/es')['ElCard']
    ElCascader: typeof import('element-plus/es')['ElCascader']
    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
    ElCol: typeof import('element-plus/es')['ElCol']
    ElCollapse: typeof import('element-plus/es')['ElCollapse']
    ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
src/components/FYImageSelectDialog.vue
@@ -56,6 +56,7 @@
            :preview-src-list="
              readonly ? typeImgMap.get(activeId).map((v) => v.url) : []
            "
            crossOrigin="Anonymous"
            :initial-index="i"
            @contextmenu="(e) => showContextMenu(e, i)"
            @click="onSelect(img, i)"
src/utils/doc.js
@@ -81,10 +81,20 @@
      let width = image.width;
      let height = image.height;
      if (width > height && horizontalHeight && height > horizontalHeight && scale) {
      if (
        width > height &&
        horizontalHeight &&
        height > horizontalHeight &&
        scale
      ) {
        height = horizontalHeight;
        width = horizontalHeight * scale;
      } else if (width <= height && verticalWidth && width > verticalWidth && scale) {
      } else if (
        width <= height &&
        verticalWidth &&
        width > verticalWidth &&
        scale
      ) {
        width = verticalWidth;
        height = verticalWidth * scale;
      }
@@ -144,12 +154,15 @@
        throw error;
      }
      const zip = new Pizzip(content);
      const imageOptions = getImageOptions(imageSize);
      let doc = new docxtemplater()
        .setOptions({ paragraphLoop: true })
        .loadZip(zip)
        .attachModule(new ImageModule(imageOptions))
        .compile();
        .loadZip(zip);
      if (imageSize) {
        const imageOptions = getImageOptions(imageSize);
        doc.attachModule(new ImageModule(imageOptions));
      }
      doc.compile();
      doc.resolveData(data).then(() => {
        try {
          doc.render();
src/views/fysp/check/ProCheckProxy.js
@@ -29,7 +29,7 @@
      //未通过数量
      unpassNum: 0,
      //整改率
      changePer: '0%',
      changePer: '--',
      //通过率
      passPer: '0%',
      //审核率
@@ -49,10 +49,10 @@
        status.uncheckNum++;
      else status.passNum++;
      if (p.extension3 == proStatus.pass) {
      if (p.extension3 != proStatus.unCheck) {
        status.proCheckedNum++;
      }
      if (p.extension3 == proStatus.change_pass) {
      if (p.extension3 == proStatus.change_pass || p.extension3 == proStatus.change_fail) {
        status.changeCheckedNum++;
      }
@@ -234,18 +234,18 @@
    else if (s.proCheckedNum < s.proNum) {
      type = 2;
    }
    // æœªæ•´æ”¹
    else if (s.changeNum < s.proNum) {
      type = 3;
    }
    // æ•´æ”¹æœªå®¡æ ¸
    else if (s.changeCheckedNum == 0) {
    else if (s.changeNum > 0 && s.changeCheckedNum == 0) {
      type = 4;
    }
    // æ•´æ”¹éƒ¨åˆ†å®¡æ ¸
    else if (s.changeCheckedNum < s.changeNum) {
    else if (s.changeNum > 0 && s.changeCheckedNum < s.changeNum) {
      type = 5;
    }
    // æœªæ•´æ”¹
    else if (s.changeNum < s.proNum) {
      type = 3;
    }
    // å®Œå…¨å®¡æ ¸
    else {
      type = 6;
src/views/fysp/check/components/ArbitraryPhoto.vue
@@ -74,6 +74,7 @@
      // ],
      closeContextMenuListenr: undefined,
      // å³é”®é€‰ä¸­çš„图片
      selectedFileElement: undefined,
      selectedFile: undefined,
      selectedIndex: undefined,
      selectedTypeId: undefined
@@ -94,7 +95,7 @@
          };
        });
      return [
        // { label: '复制图片', action: 'copy' },
        // { label: '复制到剪贴板', action: 'copy' },
        { label: '移动到', children: items }
      ];
    }
@@ -158,6 +159,7 @@
        left: `${event.clientX}px`,
        top: `${event.clientY}px`
      };
      this.selectedFileElement = event.target;
      this.selectedTypeId = typeId;
      this.selectedIndex = index;
      this.selectedFile = this.typesMap.get(typeId)[index];
@@ -168,6 +170,7 @@
    handleMenuItem(item) {
      switch (item.action) {
        case 'copy':
          this.drawImageToCanvas();
          break;
        case 'move':
          this.selectedFile.data.businesstypeid = item.value;
@@ -217,6 +220,37 @@
      //   }
      //   this.selectedFile.loading = false;
      // }, 2000);
    },
    drawImageToCanvas() {
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      const img = this.selectedFileElement; // èŽ·å–DOM引用
      // img.crossOrigin = 'Anonymous';
      canvas.width = img.naturalWidth;
      canvas.height = img.naturalHeight;
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
      // this.copyCanvasToClipboard(canvas);
      const dataUrl = canvas.toDataURL('image/png'); // å¯ä»¥é€‰æ‹©å…¶ä»–格式如'image/jpeg'
      // åˆ›å»ºä¸€ä¸ªä¸´æ—¶çš„textarea元素来复制文本
      const tempTextArea = document.createElement('textarea');
      document.body.appendChild(tempTextArea);
      tempTextArea.value = dataUrl;
      tempTextArea.select();
      document.execCommand('copy');
      document.body.removeChild(tempTextArea);
    },
    copyCanvasToClipboard(canvas) {
      canvas.toBlob((blob) => {
        const item = new ClipboardItem({ 'image/png': blob }); // ä½ å¯ä»¥æ ¹æ®éœ€è¦æ”¹å˜æ ¼å¼ï¼Œå¦‚ "image/jpeg"
        navigator.clipboard.write([item]).then(
          () => {
            console.log('Image copied to clipboard');
          },
          (err) => {
            console.error('Could not copy image: ', err);
          }
        );
      }, 'image/png'); // åŒæ ·ï¼Œè¿™é‡Œä¹Ÿå¯ä»¥æŒ‡å®šå…¶ä»–格式,如 'image/jpeg'
    }
  }
};
src/views/fysp/check/components/CompSubTaskStatistic.vue
@@ -269,17 +269,39 @@
        completedTimes: 0,
        reviewTimes: 0
      };
      this.monitorObjList.forEach((m) => {
        if (this.sceneType == undefined || m.sceneTypeId == this.sceneType) {
          _res.total++;
          const times = parseInt(m.extension1);
          if (times) {
            _res.completedScenes++;
            _res.completedTimes += times;
            _res.reviewTimes += times - 1;
      // Fixme 2025.5.30: ç›®å‰æ­¤å¤„的监管版本信息钟记录的任务完成次数有偏差,所以暂时先改为通过巡查任务本身进行统计
      /**************************************************************************************/
      // this.monitorObjList.forEach((m) => {
      //   if (this.sceneType == undefined || m.sceneTypeId == this.sceneType) {
      //     _res.total++;
      //     const times = parseInt(m.extension1);
      //     if (times) {
      //       _res.completedScenes++;
      //       _res.completedTimes += times;
      //       _res.reviewTimes += times - 1;
      //     }
      //   }
      // });
      /**************************************************************************************/
      /**************************************************************************************/
      _res.total = this.monitorObjList.filter(
        (m) => this.sceneType == undefined || m.sceneTypeId == this.sceneType
      ).length;
      _res.completedTimes = this.subtasks.length;
      const map = new Map();
      const uniqueArr = [];
      for (let item of this.subtasks) {
        if (!map.has(item.data.sceneId)) {
          map.set(item.data.sceneId, true);
          uniqueArr.push(item);
          }
        }
      });
      _res.completedScenes = uniqueArr.length;
      _res.reviewTimes = _res.completedTimes - _res.completedScenes;
      /**************************************************************************************/
      return _res;
    }
  },
src/views/fysp/scene/SceneInspectFile.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,252 @@
<template>
  <el-dialog
    :model-value="modelValue"
    @update:model-value="handleDialogChange"
    title="巡查单据下载"
    class="dialog-wrapper"
    v-loading="loading"
  >
    <el-scrollbar
      ref="scrollbarRef"
      height="50vh"
      v-loading="loading"
      :always="true"
    >
      <el-checkbox-group v-model="checkList">
        <el-space direction="vertical" alignment="flex-start" fill>
          <el-checkbox
            v-for="(item, index) in sceneInfoList"
            :key="item.scense.guid"
            :value="index"
            :class="(item.invalid ? 'checkbox-invalid' : '') + ' checkbox'"
          >
            <div>
              <el-text size="large">{{ item.scense.name }}</el-text>
            </div>
            <div class="m-t-4">
              <el-text size="small">{{
                '地址:' + item.scense.location
              }}</el-text>
            </div>
            <el-space class="m-t-4">
              <el-tag>
                {{
                  item.scense.cityname +
                  item.scense.districtname +
                  item.scense.townname
                }}
                <!-- {{ item.scense.districtname }}
                {{ item.scense.townname }} -->
              </el-tag>
              <el-tag>{{ item.scense.type }}</el-tag>
              <!-- {{ item.scense.contacts }}
              {{ item.scense.contactst }} -->
            </el-space>
          </el-checkbox>
        </el-space>
      </el-checkbox-group>
    </el-scrollbar>
    <template #footer>
      <div class="dialog-footer">
        <el-button type="danger" @click="cancel">取消</el-button>
        <el-button type="primary" :loading="docLoading" @click="handelDownload">
          ä¸‹è½½
        </el-button>
        <el-button type="default" :loading="docLoading" @click="handelPrint">
          æ‰“印
        </el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup>
/**
 * åœºæ™¯å·¡æŸ¥å•据自动下载
 */
import { ref, watch } from 'vue';
import { exportDocx } from '@/utils/doc';
import sceneApi from '@/api/fysp/sceneApi';
const props = defineProps({
  // å¯¹è¯æ¡†å¼€å…³
  modelValue: Boolean,
  // åœºæ™¯åŸºç¡€ä¿¡æ¯æ•°ç»„
  value: Array
});
const emits = defineEmits(['update:modelValue']);
const loading = ref(false);
const scrollbarRef = ref();
const sceneInfoList = ref([]);
const checkList = ref([]);
const docLoading = ref(false);
watch(
  () => [props.modelValue, props.value],
  (nV, oV) => {
    if (nV[0] && nV[1] && nV[1] != oV[1]) {
      fetchSceneInfo(nV[1]);
    }
  }
);
function fetchSceneInfo(sceneIdList) {
  loading.value = true;
  sceneInfoList.value = [];
  checkList.value = [];
  sceneIdList.forEach((sid) => {
    sceneApi
      .getSceneDetail(sid)
      .then((res) => {
        sceneInfoList.value.push(res.data);
        checkList.value.push(sceneInfoList.value.length - 1);
        //场景
        // if (res.data.scense) sceneInfoList.value = res.data.scense;
        // if (res.data.subScene) {
        //   formSubScene.value = res.data.subScene;
        // } else {
        //   formSubScene.value = {
        //     sGuid: formScene.value.guid
        //   };
        // }
      })
      .finally(() => {
        loading.value = false;
        scrollbarRef.value.setScrollTop(0);
      });
  });
}
function handleDialogChange(value) {
  emits('update:modelValue', value);
}
// æ ¼å¼åŒ–场景信息,生成参数结构
function parseParam() {
  const selected = sceneInfoList.value.filter((v, i) => {
    return checkList.value.indexOf(i) != -1;
  });
  const param = selected.map((v) => {
    switch (v.scense.typeid) {
      // å·¥åœ°
      case 1:
        return {
          type: v.scense.typeid,
          params: {
            district: v.scense.districtname,
            name: v.scense.name,
            employerUnit: v.scense.csEmployerUnit,
            constructionUnit: v.subScene.csConstructionUnit,
            timeRange: v.subScene.csStartTime
              ? `${v.subScene.csStartTime}至${v.subScene.csEndTime}`
              : undefined,
            stage: v.subScene.siExtension1,
            contacts: v.subScene.csConstructionContacts,
            contactsTel: v.subScene.csConstructionContactsTel,
            location: v.scense.location
          }
        };
      // é¤é¥®
      case 5:
        return {
          type: v.scense.typeid,
          params: {
            district: v.scense.districtname,
            location: v.scense.location,
            name: v.scense.name,
            contacts: v.scense.contacts,
            contactsTel: v.scense.contactst
          }
        };
      // default:
      //   v.invalid = true;
      //   return undefined;
    }
  });
  param.forEach((p) => {
    for (const key in p.params) {
      let value = p.params[key];
      if (value == undefined) {
        // è‹¥å±žæ€§ç¼ºå¤±ï¼Œåˆ™æ”¹ä¸º20个空格符,对应word中10个中文字符的长度
        p.params[key] = '                    ';
      }
    }
  });
  return param;
}
// æ ¹æ®åœºæ™¯ç±»åž‹ï¼Œç”Ÿæˆå¯¹åº”çš„word文档
function generateDoc(param) {
  param.forEach((p) => {
    let template, _param;
    switch (p.type) {
      // å·¥åœ°
      case 1:
        template = '/工地巡查单据模板.docx';
        _param = p.params;
        break;
      // é¤é¥®
      case 5:
        break;
      default:
        break;
    }
    exportDocx(template, _param, `${_param.name}巡查单据.docx`).finally(
      () => (docLoading.value = false)
    );
  });
}
// ä¸‹è½½word文档
function download(file) {}
// æ‰“印word文档
function print(file) {}
function filePrepare() {
  const param = parseParam();
  if (param) {
    return generateDoc(param);
  }
}
// ç‚¹å‡»ä¸‹è½½æŒ‰é’®æ“ä½œ
function handelDownload() {
  const file = filePrepare();
  if (file) {
    download(file);
  }
}
// ç‚¹å‡»æ‰“印按钮操作
function handelPrint() {
  const file = filePrepare();
  if (file) {
    print(file);
  }
}
// å–消操作
function cancel() {
  // å…³é—­å¯¹è¯æ¡†
  handleDialogChange(false);
}
</script>
<style scoped>
.checkbox {
  border: var(--el-border);
  padding: 8px;
}
.checkbox-invalid {
  border-color: var(--el-color-error);
}
:deep(.el-checkbox) {
  height: auto;
}
</style>
src/views/fysp/task/TaskManage.vue
@@ -202,10 +202,16 @@
  computed: {
    // æ€»ä»»åŠ¡çŠ¶æ€ç»Ÿè®¡
    taskStatus() {
      let total = 0,
        inspected = 0;
      this.curMonitorObjList.forEach((obj) => {
        total += parseInt(obj.monitornum);
        inspected += obj.extension1 ? parseInt(obj.extension1) : 0;
      });
      return [
        { name: '场景数', value: 100 },
        { name: '未巡查', value: 0 },
        { name: '已巡查', value: 0 }
        { name: '场景数', value: total },
        { name: '未巡查', value: total - inspected },
        { name: '已巡查', value: inspected }
      ];
    }
  },
src/views/fysp/task/components/CompMonitorObjEdit.vue
@@ -274,7 +274,7 @@
      });
    },
    deleteMov(item) {
      if (item.extension1) {
      if (!(item.extension1 == null || item.extension1 == undefined || item.extension1 == '0')) {
        ElMessage({
          message: '已监管场景无法移除',
          type: 'error'
src/views/fysp/task/components/CompSubTaskList.vue
@@ -1,6 +1,14 @@
<template>
  <el-row justify="space-between">
    <el-text>单日计划</el-text>
    <div>
      <el-button
        type="success"
        size="small"
        plain
        @click="handleInspectFileDownload"
        >单据下载</el-button
      >
    <el-button
      v-show="create && data && data.length > 0"
      type="success"
@@ -8,6 +16,7 @@
      @click="add"
      >任务调整</el-button
    >
    </div>
  </el-row>
  <el-divider />
  <div>
@@ -70,11 +79,13 @@
      @cancel="dialogVisible = false"
    ></CompSubTaskEdit>
  </el-dialog>
  <SceneInspectFile v-model="downloadDialog" :value="downloadSceneList"></SceneInspectFile>
</template>
<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { ElMessageBox, ElNotification, ElMessage } from 'element-plus';
import CompSubTaskEdit from './CompSubTaskEdit.vue';
import SceneInspectFile from "@/views/fysp/scene/SceneInspectFile.vue";
import subtaskApi from '@/api/fysp/subtaskApi';
const props = defineProps({
@@ -86,12 +97,14 @@
  // æ˜¯å¦æ˜¾ç¤ºæ·»åŠ ä»»åŠ¡æŒ‰é’®
  create: Boolean,
  loading: Boolean,
  createLoading:Boolean,
  createLoading: Boolean
});
const dialogVisible = ref(false);
const activeItem = ref(null);
const data = computed(() => props.modelValue);
const downloadDialog = ref(false);
const downloadSceneList = ref([])
const emit = defineEmits(['submit', 'add', 'remove', 'update:modelValue']);
@@ -102,7 +115,7 @@
      cancelButtonText: '取消',
      type: 'warning'
    }).then(() => {
      return subtaskApi.deleteSubtask(item.stguid).then(res=>{
      return subtaskApi.deleteSubtask(item.stguid).then((res) => {
        if (res == 1) {
          const index = data.value.indexOf(item);
          data.value.splice(index, 1);
@@ -110,9 +123,9 @@
          emit('update:modelValue', data.value);
          emit('remove', item);
        } else {
          Promise.reject('删除巡查任务失败')
          Promise.reject('删除巡查任务失败');
        }
      })
      });
    });
  }
}
@@ -139,4 +152,9 @@
onUnmounted(() => {
  dialogVisible.value = false;
});
function handleInspectFileDownload() {
  downloadSceneList.value = data.value.map(v=>v.scenseid)
  downloadDialog.value = true
}
</script>