瀏覽代碼

✨ feat(ruidu): 平台井关联日报

Zimo 1 月之前
父節點
當前提交
b906ff8783
共有 5 個文件被更改,包括 1801 次插入313 次删除
  1. 22 18
      api/ruiDu.js
  2. 995 0
      pages/ruiDu/compontents/report-form-copy.vue
  3. 714 213
      pages/ruiDu/compontents/report-form.vue
  4. 58 70
      pages/ruiDu/edit.vue
  5. 12 12
      store/modules/dataDict.js

+ 22 - 18
api/ruiDu.js

@@ -1,5 +1,4 @@
-import { request, upload } from "@/utils/request";
-import config from "@/utils/config";
+import { request, upload } from '@/utils/request';
 
 /**
  * 获取瑞都日报列表
@@ -9,8 +8,8 @@ import config from "@/utils/config";
  */
 export function getRuiDuReportPage(params) {
   return request({
-    url: "/pms/iot-rd-daily-report/page",
-    method: "get",
+    url: '/pms/iot-rd-daily-report/page',
+    method: 'get',
     params,
   });
 }
@@ -22,8 +21,8 @@ export function getRuiDuReportPage(params) {
  */
 export function getRuiDuReportDetail(params) {
   return request({
-    url: "/pms/iot-rd-daily-report/get",
-    method: "get",
+    url: '/pms/iot-rd-daily-report/get',
+    method: 'get',
     params,
   });
 }
@@ -34,8 +33,8 @@ export function getRuiDuReportDetail(params) {
  */
 export function getRuiDuReportAttrs(params) {
   return request({
-    url: "/rq/iot-daily-report-attrs/dailyReportAttrs",
-    method: "get",
+    url: '/rq/iot-daily-report-attrs/dailyReportAttrs',
+    method: 'get',
     params,
   });
 }
@@ -44,18 +43,15 @@ export function getRuiDuReportAttrs(params) {
  * 上传瑞都日报附件
  * @param filePath
  */
-export const uploadAttachmentsFile = (
-  filePath,
-  deviceId = undefined
-) =>
-  upload("/rq/file/upload", {
+export const uploadAttachmentsFile = (filePath, deviceId = undefined) =>
+  upload('/rq/file/upload', {
     // #ifdef MP-ALIPAY
-    fileType: "image/video/audio", // 仅支付宝小程序,且必填。
+    fileType: 'image/video/audio', // 仅支付宝小程序,且必填。
     // #endif
     filePath: filePath, // 要上传文件资源的路径。
-    name: "files", // 文件对应的 key , 开发者在服务器端通过这个 key 可以获取到文件二进制内容
+    name: 'files', // 文件对应的 key , 开发者在服务器端通过这个 key 可以获取到文件二进制内容
     header: {
-      "device-id": deviceId,
+      'device-id': deviceId,
     } /* 会与全局header合并,如有同名属性,局部覆盖全局 */,
   });
 
@@ -65,8 +61,16 @@ export const uploadAttachmentsFile = (
  */
 export function updateRuiDuReport(data) {
   return request({
-    url: "/pms/iot-rd-daily-report/update",
-    method: "put",
+    url: '/pms/iot-rd-daily-report/update',
+    method: 'put',
+    data,
+  });
+}
+
+export function updateRuiDuReportBatch(data) {
+  return request({
+    url: '/pms/iot-rd-daily-report/saveBatch',
+    method: 'post',
     data,
   });
 }

+ 995 - 0
pages/ruiDu/compontents/report-form-copy.vue

@@ -0,0 +1,995 @@
+<template>
+  <scroll-view scroll-y="true" class="report-form">
+    <!-- 日报详情 - 日报信息 -->
+    <view class="report-form-content form-content">
+      <uni-forms
+        ref="reportFormRef"
+        labelWidth="140px"
+        :modelValue="formData"
+        :rules="formDataRules"
+        err-show-type="toast">
+        <!-- 时间节点 -->
+        <uni-forms-item class="form-item" :label="`${$t('ruiDu.timeNode')}:`" :required="isRequired">
+          <view
+            class="time-range-container flex-row align-center justify-end"
+            @click="props.formDisable ? '' : handleClickTimeRange()">
+            <view class="time-range-item" v-if="formData.startTime && formData.endTime">
+              {{ formData.startTime }} 至 {{ formData.endTime }}
+            </view>
+            <view class="time-range-item" v-else>
+              {{ selectPlaceholder }}
+            </view>
+          </view>
+        </uni-forms-item>
+        <!-- 施工状态 -->
+        <uni-forms-item
+          class="form-item"
+          :label="`${$t('ruiDu.constructionStatus')}:`"
+          :required="isRequired"
+          name="rdStatus">
+          <uni-data-select
+            class="form-item-select align-center"
+            :clear="false"
+            :align="'right'"
+            :localdata="rdStatusRange"
+            :placeholder="selectPlaceholder"
+            :disabled="props.formDisable"
+            v-model="formData.rdStatus">
+          </uni-data-select>
+        </uni-forms-item>
+        <!-- 施工设备 -->
+        <uni-forms-item class="form-item" :label="`${$t('ruiDu.constructionEquipment')}:`">
+          <view class="flex-col">
+            <uni-row class="flex-row align-center justify-end">
+              <uni-col :span="6" class="flex-col align-center justify-end">
+                <button
+                  class="mini-btn form-item-btn align-center justify-center"
+                  type="primary"
+                  :disabled="props.formDisable"
+                  v-if="!props.formDisable"
+                  @click="handleClickSelectDevice">
+                  {{ $t('device.selectDevice') }}
+                </button>
+              </uni-col>
+            </uni-row>
+            <uni-row>
+              <uni-easyinput
+                style="text-align: right"
+                type="textarea"
+                :autoHeight="true"
+                :inputBorder="false"
+                :clearable="false"
+                :styles="{ disableColor: '#fff' }"
+                :placeholder="$t('ruiDu.unselectedEquipment')"
+                :disabled="true"
+                v-model="selectedEquipmentNames" />
+            </uni-row>
+          </view>
+        </uni-forms-item>
+        <!-- 未选择设备 -->
+        <uni-forms-item class="form-item" :label="`${$t('ruiDu.unselectedEquipment')}:`">
+          <uni-easyinput
+            style="text-align: right"
+            type="textarea"
+            :autoHeight="true"
+            :inputBorder="false"
+            :clearable="false"
+            :styles="{ disableColor: '#fff' }"
+            :placeholder="' '"
+            :disabled="true"
+            v-model="unselectedEquipmentNames" />
+        </uni-forms-item>
+        <!-- 施工工艺 -->
+        <uni-forms-item
+          class="form-item"
+          :label="`${$t('ruiDu.constructionProcess')}:`"
+          :required="isRequired"
+          name="techniqueIds">
+          <uni-data-select
+            ref="uniDataSelect"
+            mode="underline"
+            placement="bottom"
+            :multiple="true"
+            :clear="false"
+            :localdata="techniqueRange"
+            :disabled="props.formDisable"
+            v-model="formData.techniqueIds">
+            <template v-slot:selected="{ selectedItems }" class="form-item">
+              <view class="slot-box flex-row align-center justify-end">
+                <view
+                  v-for="item in selectedItems"
+                  :key="item.value"
+                  class="slot-content-item selected align-center justify-start">
+                  {{ item.text }}
+                  <uni-icons
+                    type="close"
+                    size="18"
+                    color="#909399"
+                    v-if="!props.formDisable"
+                    @click="removeSelectedItem(item.value)"></uni-icons>
+                </view>
+                <view v-if="selectedItems.length == 0" class="slot-content-item align-center justify-start">
+                  {{ selectPlaceholder }}
+                </view>
+              </view>
+            </template>
+            <template v-slot:option="{ item, itemSelected }">
+              <view class="slot-item">
+                <uni-list-item class="slot-list-item flex-row align-center justify-between">
+                  <template v-slot:body>
+                    <text class="slot-item-text justify-start" :style="{ color: itemSelected ? '#007aff' : '#303133' }">
+                      {{ item.text }}
+                    </text>
+                  </template>
+                  <template v-slot:footer>
+                    <uni-icons
+                      class="align-center justify-center"
+                      v-if="itemSelected"
+                      type="checkmarkempty"
+                      size="20"
+                      color="#007aff"></uni-icons>
+                  </template>
+                </uni-list-item>
+              </view>
+            </template>
+            <!-- 可以拦截点击事件之后自定义
+					<template v-slot:option="{item,itemSelected}">
+						<view @click.stop>
+							<uni-list-item showSwitch :switchChecked="itemSelected" :title="item.text" :note="item.value+''"
+								:disabled="item.disable" @switchChange="switchChange($event,item)"></uni-list-item>
+						</view>
+					</template> -->
+            <template v-slot:empty>
+              <view class="empty-box">
+                <view>{{ $t('common.noData') }}</view>
+              </view>
+            </template>
+          </uni-data-select>
+        </uni-forms-item>
+
+        <!--当日生产动态 -->
+        <uni-forms-item
+          class="form-item"
+          :label="`${$t('ruiDu.dailyProductionDynamic')}:`"
+          :required="isRequired"
+          name="productionStatus">
+          <uni-easyinput
+            style="text-align: right"
+            type="textarea"
+            :autoHeight="true"
+            :inputBorder="false"
+            :clearable="false"
+            :styles="{ disableColor: '#fff' }"
+            :placeholder="inputPlaceholder"
+            :disabled="props.formDisable"
+            v-model="formData.productionStatus" />
+        </uni-forms-item>
+        <!-- 下步工作计划 -->
+        <uni-forms-item class="form-item" :label="`${$t('ruiDu.nextWorkPlan')}:`" name="nextPlan">
+          <uni-easyinput
+            style="text-align: right"
+            type="textarea"
+            :autoHeight="true"
+            :inputBorder="false"
+            :clearable="false"
+            :styles="{ disableColor: '#fff' }"
+            :placeholder="inputPlaceholder"
+            :disabled="props.formDisable"
+            v-model="formData.nextPlan" />
+        </uni-forms-item>
+        <!-- 外租设备 -->
+        <uni-forms-item class="form-item" :label="`${$t('ruiDu.externalRentalEquipment')}:`" name="externalRental">
+          <uni-easyinput
+            style="text-align: right"
+            type="textarea"
+            :autoHeight="true"
+            :inputBorder="false"
+            :clearable="false"
+            :styles="{ disableColor: '#fff' }"
+            :placeholder="inputPlaceholder"
+            :disabled="props.formDisable"
+            v-model="formData.externalRental" />
+        </uni-forms-item>
+        <!-- 故障情况 -->
+        <uni-forms-item class="form-item" :label="`${$t('ruiDu.faultSituation')}:`" name="malfunction">
+          <uni-easyinput
+            style="text-align: right"
+            type="textarea"
+            :autoHeight="true"
+            :inputBorder="false"
+            :clearable="false"
+            :styles="{ disableColor: '#fff' }"
+            :placeholder="inputPlaceholder"
+            :disabled="props.formDisable"
+            v-model="formData.malfunction" />
+        </uni-forms-item>
+        <!-- 故障误工H -->
+        <uni-forms-item class="form-item" :label="`${$t('ruiDu.faultDowntimeH')}:`" name="faultDowntime">
+          <uni-easyinput
+            class="digit-item"
+            type="digit"
+            :inputBorder="false"
+            :clearable="true"
+            :styles="{ disableColor: '#fff' }"
+            :placeholder="inputPlaceholder"
+            :disabled="props.formDisable"
+            v-model="formData.faultDowntime" />
+        </uni-forms-item>
+        <!-- 附件 -->
+        <uni-forms-item class="form-item" :label="`${$t('ruiDu.attachment')}:`" :required="false" name="attachments">
+          <uni-file-picker
+            file-mediatype="all"
+            :clear="false"
+            :limit="9"
+            :auto-upload="false"
+            :readonly="props.formDisable"
+            v-model="attachmentsFileList"
+            @select="uploadFiles"
+            @delete="deleteFiles"
+            v-if="!props.formDisable">
+            <view class="file-picker-container flex-row align-end justify-end">
+              <view class="file-size-limit">
+                {{ $t('ruiDu.fileSizeLimit') }}
+              </view>
+              <button type="primary" size="mini" class="file-picker-btn">
+                {{ $t('ruiDu.selectFile') }}
+              </button>
+            </view>
+          </uni-file-picker>
+          <view class="file-list flex-col justify-start" v-else>
+            <uni-row
+              class="file-item flex-row align-center justify-between"
+              v-for="(file, index) in attachmentsFileList"
+              :key="index">
+              <uni-col :span="20" class="file-name flex-row">{{ file.name }}</uni-col>
+              <uni-col :span="3" class="file-btn align-center" @click="downloadFile(file)">
+                {{ $t('operation.download') }}
+              </uni-col>
+            </uni-row>
+          </view>
+        </uni-forms-item>
+        <!--  ---------------  以下是根据施工工艺获取的工作量字段  ------------------ -->
+        <uni-forms-item
+          class="form-item"
+          v-for="(attr, index) in formData.extProperty"
+          :key="index"
+          :label="`${attr.name}(${attr.unit}):`"
+          :required="attr.required == 1 && isRequired"
+          :name="`extProperty[${index}].actualValue`">
+          <uni-easyinput
+            v-if="attr.dataType === 'double'"
+            class="digit-item"
+            type="digit"
+            :inputBorder="false"
+            :clearable="true"
+            :styles="{ disableColor: '#fff' }"
+            :placeholder="inputPlaceholder"
+            :disabled="props.formDisable"
+            v-model="attr.actualValue" />
+          <uni-easyinput
+            v-else
+            style="text-align: right"
+            :styles="{ disableColor: '#fff' }"
+            :inputBorder="false"
+            :clearable="true"
+            :placeholder="inputPlaceholder"
+            :disabled="props.formDisable"
+            v-model="attr.actualValue"
+            :type="'textarea'"></uni-easyinput>
+        </uni-forms-item>
+      </uni-forms>
+    </view>
+  </scroll-view>
+  <!-- 时间范围选择器 (时:分)-->
+  <tpf-time-range
+    ref="timeRangeRef"
+    :startTime="startTime"
+    :startDefaultTime="startDefaultTime"
+    :endTime="endTime"
+    :endDefaultTime="endDefaultTime"
+    @timeRange="timeRange"></tpf-time-range>
+  <!-- 设备选择器 (穿梭框) -->
+  <device-transfer
+    ref="deviceTransferRef"
+    :allList="reportData.selectedDevices"
+    :selected="formData.deviceIds"
+    @confirm="handleTransferChange" />
+</template>
+
+<script setup>
+  import { onLoad } from '@dcloudio/uni-app';
+  import { ref, reactive, computed, getCurrentInstance, onMounted, nextTick, watch } from 'vue';
+
+  // -------------------------- 引入api接口 start--------------------------
+  import { getRuiDuReportAttrs, updateRuiDuReport, uploadAttachmentsFile } from '@/api/ruiDu.js';
+  // -------------------------- 引入api接口 end  --------------------------
+  // --------------------------引用组件-----start---------------------------
+  import tpfTimeRange from '@/components/tpf-time-range/tpf-time-range.vue';
+  import deviceTransfer from '@/components/device-transfer/index.vue';
+  // --------------------------引用组件-----end-----------------------------
+  // --------------------------引用全局变量$t-------------------------------
+  const { appContext } = getCurrentInstance();
+  const t = appContext.config.globalProperties.$t;
+  // --------------------------引用字典项-----------------------------------
+  import { useDataDictStore } from '@/store/modules/dataDict';
+  const { getIntDictOptions, getStrDictOptions } = useDataDictStore();
+
+  // --------------------------字典项-----------------------------------
+  // 施工状态
+  const rdStatusRange = getStrDictOptions('rdStatus').map(item => {
+    return {
+      ...item,
+      text: item.label,
+    };
+  });
+  console.log('🚀 ~ 施工状态:', rdStatusRange);
+  // 施工工艺
+  const techniqueRange = getIntDictOptions('rq_iot_project_technology_rd').map(item => {
+    return {
+      ...item,
+      text: item.label,
+    };
+  });
+  console.log('🚀 ~ 施工工艺:', techniqueRange);
+
+  // -------------------------接收父组件传递的参数--------------------------
+  const props = defineProps({
+    reportId: {
+      type: String,
+      default: '',
+    },
+    reportData: {
+      type: Object,
+      default: () => {},
+    },
+    formDisable: {
+      type: Boolean,
+      default: false, // 是否禁用表单
+    },
+  });
+
+  // -------------------------- 生命周期函数 --------------------------
+  onMounted(() => {
+    console.log('🚀 ~ report-form onMounted ~ props:', props);
+    // 初始化表单数据
+    // formDataFormat();
+  });
+  onLoad(options => {
+    console.log('🚀 ~ report-form onLoad ~ options:', options);
+  });
+
+  // -------------------------- 页面变量 --------------------------
+  // 表单ref
+  const reportFormRef = ref(null);
+  // 选择占位符
+  const selectPlaceholder = computed(() => {
+    return props.formDisable ? ' ' : t('operation.PleaseSelect');
+  });
+  // 输入占位符
+  const inputPlaceholder = computed(() => {
+    return props.formDisable ? ' ' : t('operation.PleaseInput');
+  });
+  // 表单项是否必填 required
+  const isRequired = computed(() => {
+    return props.formDisable ? false : true;
+  });
+
+  // ---------------时间范围---------------
+  const timeRangeRef = ref(null);
+  const startTime = ref('00:00');
+  const startDefaultTime = ref('06:00');
+  const endTime = ref('24:00');
+  const endDefaultTime = ref('06:00');
+  // ------------- 施工设备 ---------------
+  // 设备选择器
+  const deviceTransferRef = ref(null);
+  // 已选择的设备(名称)
+  const selectedEquipmentNames = ref('');
+  // 未选择的设备(名称)
+  const unselectedEquipmentNames = ref('');
+  // 上传的附件文件列表
+  const attachmentsFileList = ref([]);
+
+  // 表单数据
+  const formData = ref({
+    startTime: startDefaultTime.value, //时间节点 - 开始时间
+    endTime: endDefaultTime.value, //时间节点 - 结束时间
+    rdStatus: '', //施工状态
+    deviceIds: [], //施工设备
+    techniqueIds: [], //施工工艺
+    productionStatus: '', //当日生产动态
+    attachments: [], // 附件
+    extProperty: [], // 扩展属性
+  });
+  console.log('🚀 ~ formData:', formData);
+  // 表单校验规则
+  const formDataBaseRules = reactive({
+    // 时间节点 - 开始时间
+    startTime: {
+      rules: [
+        {
+          required: true,
+          errorMessage: `${t('operation.PleaseSelect')}${t('ruiDu.timeNode')}`,
+        },
+      ],
+    },
+    // 时间节点 - 结束时间
+    endTime: {
+      rules: [
+        {
+          required: true,
+          errorMessage: `${t('operation.PleaseSelect')}${t('ruiDu.timeNode')}`,
+        },
+      ],
+    },
+    // 施工状态
+    rdStatus: {
+      rules: [
+        {
+          required: true,
+          errorMessage: `${t('operation.PleaseSelect')}${t('ruiDu.constructionStatus')}`,
+        },
+      ],
+    },
+    // 施工工艺
+    techniqueIds: {
+      rules: [
+        {
+          required: true,
+          errorMessage: `${t('operation.PleaseSelect')}${t('ruiDu.constructionProcess')}`,
+        },
+      ],
+    },
+    // 当日生产动态
+    productionStatus: {
+      rules: [
+        {
+          required: true,
+          errorMessage: `${t('operation.PleaseFillIn')}${t('ruiDu.dailyProductionDynamic')}`,
+        },
+      ],
+    },
+  });
+  // 动态计算表单校验规则
+  const formDataRules = computed(() => {
+    const rules = { ...formDataBaseRules };
+    // 根据施工工艺动态添加工作量属性字段的校验规则
+    formData.value.extProperty.forEach((attr, index) => {
+      if (attr.required == 1) {
+        rules[`extProperty[${index}].actualValue`] = {
+          rules: [
+            {
+              required: true,
+              errorMessage: `${t('operation.PleaseFillIn')}${attr.name}`,
+            },
+          ],
+        };
+      }
+    });
+    console.log('生成的校验规则:', rules);
+    return rules;
+  });
+  // -------------------------- 页面方法  --------------------------
+
+  // 表单校验方法
+  const validate = async () => {
+    // 同步最新的formDataRules(解决computed延迟)
+    const latestRules = formDataRules.value;
+    await nextTick(); // 等待DOM与规则同步
+    reportFormRef.value.setRules(latestRules);
+    console.log('基础校验通过(formDataRules已生效)');
+    // 调用uni-forms的validate方法进行校验
+    return await reportFormRef.value.validate();
+  };
+  // 表单提交
+  const submitForm = async () => {
+    console.log('🚀 ~ formDataRules:', formDataRules);
+    console.log('🚀 ~ submitForm ~ formData.value:', formData.value);
+
+    const formValid = await validate();
+    if (formValid) {
+      // 校验施工工艺的工作量属性字段是否填写
+      formData.value.extProperty.forEach(attr => {
+        if (attr.required == 1 && !attr.actualValue) {
+          uni.showToast({
+            title: `${t('operation.PleaseFillIn')}${attr.name}(${attr.unit})`,
+            icon: 'none',
+          });
+          return false;
+        }
+      });
+    }
+    // 处理表单数据
+    const formDataCopy = JSON.parse(JSON.stringify(formData.value));
+    // 处理施工工艺
+    formDataCopy.extProperty.forEach(attr => {
+      if (attr.dataType === 'double') {
+        attr.actualValue = Number(attr.actualValue);
+      }
+    });
+    console.log('🚀 ~ submitForm ~ formDataCopy:', formDataCopy);
+    // 提交表单
+    updateRuiDuReport({
+      id: props.reportId,
+      companyId: props.reportData.companyId,
+      deptId: props.reportData.deptId,
+      ...formDataCopy,
+    }).then(res => {
+      // 提交成功
+      if (res.code === 0) {
+        uni.showToast({ title: t('operation.success'), icon: 'none' });
+        // 返回上一页
+        uni.navigateBack();
+      } else {
+        uni.showToast({ title: res.msg, icon: 'none' });
+      }
+    });
+  };
+  // 点击时间范围选择
+  const handleClickTimeRange = () => {
+    console.log('🚀 ~ handleClickTimeRange ~ timeRangeRef.value:', timeRangeRef.value);
+    // 打开时间范围选择器
+    timeRangeRef.value.open();
+  };
+  // 时间范围选择回调
+  const timeRange = data => {
+    console.log('🚀 ~ timeRange ~ data:', data);
+    formData.value.startTime = data[0];
+    formData.value.endTime = data[1];
+  };
+  // 初始化表单数据
+  const formDataFormat = () => {
+    // 处理时间范围
+    timeRangeFormat();
+    // 处理已选择的设备
+    if (props.reportData?.deviceIds) {
+      const { deviceIds = [] } = props.reportData || {};
+      handleEquipmentNames(deviceIds);
+    }
+
+    // 处理其他表单数据
+    // 施工状态
+    formData.value.rdStatus = props.reportData.rdStatus || ''; //施工状态
+    // 施工工艺
+    formData.value.techniqueIds = props.reportData.techniqueIds || []; //施工工艺
+    // 当日生产动态
+    formData.value.productionStatus = props.reportData.productionStatus || ''; //当日生产动态
+    // 下步工作计划
+    formData.value.nextPlan = props.reportData.nextPlan || ''; //下步工作计划
+    // 外租设备
+    formData.value.externalRental = props.reportData.externalRental || ''; //外租设备
+    // 故障情况
+    formData.value.malfunction = props.reportData.malfunction || ''; //故障情况
+    // 故障误工H
+    formData.value.faultDowntime = props.reportData.faultDowntime || ''; //故障误工H
+    // 附件
+    formData.value.attachments = props.reportData.attachments || [];
+    // 展示用的文件列表
+    attachmentsFileList.value =
+      props.reportData?.attachments?.map(item => ({
+        ...item,
+        name: item.filename,
+        url: item.filePath,
+      })) || [];
+    // 施工工艺对应的工作量属性字段
+    formData.value.extProperty = props.reportData.extProperty || [];
+    console.log('🚀 ~ formDataFormat ~ formData.value:', formData.value);
+  };
+  // 初始化时间范围
+  const timeRangeFormat = () => {
+    // 处理时间范围:将[8,0]转换成"08:00"
+    console.log('🚀 ~ timeRangeFormat ~ props:', props.reportData.startTime);
+    if (props.reportData.startTime) {
+      const startArr = props.reportData.startTime;
+      formData.value.startTime = `${startArr[0].toString().padStart(2, '0')}:${startArr[1]
+        .toString()
+        .padStart(2, '0')}`;
+    }
+    if (props.reportData.endTime) {
+      const endArr = props.reportData.endTime;
+      formData.value.endTime = `${endArr[0].toString().padStart(2, '0')}:${endArr[1].toString().padStart(2, '0')}`;
+    }
+  };
+  // 处理施工设备及未选择设备名称
+  const handleEquipmentNames = deviceIds => {
+    console.log('🚀 ~ handleEquipmentNames ~ deviceIds:', deviceIds);
+    formData.value.deviceIds = deviceIds || []; //施工设备
+    console.log('🚀 ~ handleEquipmentNames ~ formData.value.deviceIds:', formData.value.deviceIds);
+    const { selectedDevices = [] } = props.reportData || {};
+    const deviceIdSet = new Set(deviceIds);
+    //   已选择的设备(名称)
+    selectedEquipmentNames.value = selectedDevices
+      .filter(item => deviceIdSet.has(item.id))
+      .map(item => item.deviceName)
+      .join(',');
+
+    //   未选择的设备(名称)
+    unselectedEquipmentNames.value =
+      selectedDevices
+        .filter(item => !deviceIdSet.has(item.id))
+        .map(item => item.deviceName)
+        .join(',') || t('ruiDu.allEquipmentConstructed');
+  };
+  // 点击施工设备选择器
+  const handleClickSelectDevice = () => {
+    deviceTransferRef.value.open();
+  };
+  // 设备选择器回调
+  const handleTransferChange = selectedIds => {
+    console.log('🚀 ~ handleTransferChange ~ selectedIds:', selectedIds);
+    // 更新已选择的设备及名称
+    handleEquipmentNames(selectedIds);
+  };
+  // 移除已选择的施工工艺
+  const removeSelectedItem = value => {
+    console.log('🚀 ~ removeSelectedItem ~ value:', value);
+    formData.value.techniqueIds = formData.value.techniqueIds.filter(item => item !== value);
+  };
+
+  // 根据已选择的施工工艺对应的工作量属性字段
+  const getWorkloadInfoByTechnique = () => {
+    getRuiDuReportAttrs({
+      techniqueIds: formData.value.techniqueIds.join(','),
+    }).then(res => {
+      console.log('🚀 ~ getWorkloadInfoByTechnique ~ res:', res);
+      const { data = [] } = res;
+
+      // 1. 按 "identifier+unit" 去重:用Map保证唯一,key为拼接字段
+      const uniqueMap = new Map();
+      data.forEach(item => {
+        // 生成去重key(identifier和unit都存在才拼接,避免异常)
+        const key =
+          item.identifier && item.unit ? `${item.identifier}-${item.unit}` : Math.random().toString(36).slice(2, 11); // 异常情况用随机key避免重复
+        uniqueMap.set(key, item); // 重复key会覆盖,实现去重
+      });
+      // 去重后的数组
+      const uniqueData = Array.from(uniqueMap.values());
+
+      // 2. 对比formData.extProperty,保留已有actualValue(避免覆盖用户输入)
+      const handledData = uniqueData.map(newItem => {
+        // 生成当前新项的去重key
+        const newKey = newItem.identifier && newItem.unit ? `${newItem.identifier}-${newItem.unit}` : '';
+
+        // 在原有extProperty中找匹配项
+        const oldItem = formData.value.extProperty.find(old => {
+          const oldKey = old.identifier && old.unit ? `${old.identifier}-${old.unit}` : '';
+          return newKey && oldKey && newKey === oldKey;
+        });
+
+        // 有匹配项则复用原有actualValue,无则用新项的(默认空)
+        return {
+          ...newItem,
+          actualValue: oldItem?.actualValue ?? newItem.actualValue,
+        };
+      });
+
+      // 3. 重新赋值给formData.extProperty(更新页面展示)
+      formData.value.extProperty = handledData;
+
+      console.log('formData.value.extProperty :>> ', formData.value.extProperty);
+    });
+  };
+  // 上传附件(超过50M提示并删除文件列表展示)
+  const uploadFiles = async event => {
+    console.log('🚀 ~ uploadFiles ~ event:', event);
+    const tempFiles = event.tempFiles || []; // 选择的临时文件列表
+    const maxSize = 50 * 1024 * 1024; // 50M(字节)
+    const overSizeFiles = []; // 存储超过50M的文件
+
+    // 1. 筛选超过50M的文件,记录文件名
+    tempFiles.forEach(file => {
+      if (file.size > maxSize) {
+        overSizeFiles.push(file.name);
+      }
+    });
+
+    // 2. 有超过50M的文件:提示+从列表中移除
+    if (overSizeFiles.length > 0) {
+      // 提示信息
+      uni.showToast({
+        title: `【${overSizeFiles.join('、')}】${t('ruiDu.fileSizeLimit')}`,
+        icon: 'none',
+        duration: 3000,
+      });
+
+      // 关键:从formData.attachments中移除超过50M的文件(根据文件名匹配)
+      formData.value.attachments = formData.value.attachments.filter(attach => !overSizeFiles.includes(attach.name));
+
+      // 兼容:强制更新组件状态(避免uni-file-picker仍展示已移除文件)
+      await nextTick();
+      return; // 终止后续上传逻辑
+    }
+
+    // 3. 所有文件大小合法,执行正常上传逻辑(原有代码保留)
+    try {
+      for (const file of tempFiles) {
+        const uploadTask = await uploadAttachmentsFile(file.path, undefined);
+        console.log('🚀 ~ uploadFiles ~ uploadTask:', uploadTask);
+        if (uploadTask.code === 0) {
+          console.log('上传成功:', uploadTask);
+          const { data } = uploadTask;
+          // 根据返回结果更新附件信息
+          formData.value.attachments.push({
+            bizId: props.reportId,
+            category: 'daily_report',
+            filePath: data.files[0].filePath,
+            filename: data.files[0].name,
+            // fileSize: data.files[0].size,
+            // fileType: data.files[0].type,
+            remark: '',
+            type: 'attachment',
+          });
+          // 更新展示用的文件列表
+          attachmentsFileList.value.push({
+            ...data.files[0],
+            name: data.files[0].name,
+            url: data.files[0].filePath,
+          });
+        } else {
+          console.error('上传失败:', uploadTask);
+          uni.showToast({
+            title: `【${file.name}】${t('operation.uploadFail')}`,
+            icon: 'none',
+          });
+        }
+      }
+    } catch (err) {
+      console.error('上传异常:', err);
+      uni.showToast({ title: '上传异常,请重试', icon: 'none' });
+    }
+  };
+  // 删除附件
+  const deleteFiles = event => {
+    console.log('🚀 ~ deleteFiles ~ event:', event);
+    const { tempFile, index } = event;
+    // 1. 从formData.attachments中移除选中项
+    formData.value.attachments.splice(index, 1);
+    // 2. 从展示用的文件列表中移除选中项
+    attachmentsFileList.value.splice(index, 1);
+  };
+  // 下载文件
+  const downloadFile = async file => {
+    console.log('🚀 ~ downloadFile ~ file:', file);
+    const { url: fileUrl, name: fileName } = file;
+    if (!fileUrl) {
+      uni.showToast({ title: t('operation.fileUrlEmpty'), icon: 'none' });
+      return;
+    }
+    // 获取平台
+    const platform = uni.getSystemInfoSync().platform;
+    console.log('🚀 ~ downloadFile ~ platform:', platform);
+    // 判断平台
+    if (platform === 'android') {
+      uni.downloadFile({
+        url: fileUrl,
+        success: res => {
+          console.log('🚀 ~ downloadFile ~ res:', res);
+          if (res.statusCode === 200) {
+            uni.saveFile({
+              tempFilePath: res.tempFilePath,
+              success: res => {
+                console.log('🚀 ~ downloadFile saveFile ~ res:', res);
+                uni.showToast({
+                  title: t('operation.downloadSuccess'),
+                  icon: 'none',
+                });
+              },
+              fail: err => {
+                console.log('🚀 ~ downloadFile saveFile ~ err:', err);
+                uni.showToast({
+                  title: t('operation.downloadFail'),
+                  icon: 'none',
+                });
+              },
+            });
+          }
+        },
+        fail: err => {
+          console.log('🚀 ~ downloadFile ~ err:', err);
+          uni.showToast({
+            title: t('operation.downloadFail'),
+            icon: 'none',
+          });
+        },
+      });
+    } else {
+      try {
+        // 2.1 处理文件名(避免特殊字符乱码,如中文、空格)
+        const safeFileName = decodeURIComponent(fileName || '未命名文件'); // 解码URL编码的文件名
+
+        // 2.2 创建隐藏的 <a> 标签(核心:利用 download 属性触发下载)
+        const link = document.createElement('a');
+        // 关键:设置 download 属性指定文件名(Web 端独有)
+        link.download = safeFileName;
+        // 设置文件地址(若跨域,需后端配置 CORS + Content-Disposition 响应头)
+        link.href = fileUrl;
+        // 隐藏 <a> 标签(不影响页面布局)
+        link.style.display = 'none';
+        // 将 <a> 标签添加到文档中(否则部分浏览器无法触发点击)
+        document.body.appendChild(link);
+
+        // 2.3 模拟点击 <a> 标签触发下载
+        link.click();
+
+        // 2.4 下载后清理资源(避免内存泄漏)
+        setTimeout(() => {
+          document.body.removeChild(link); // 移除 <a> 标签
+          URL.revokeObjectURL(link.href); // 释放 URL 资源(若使用 Blob 时必需)
+        }, 100);
+      } catch (err) {
+        console.error('🚀 ~ Web 端下载失败:', err);
+        // 若直接通过 <a> 标签下载失败(如跨域),尝试通过 Blob 流下载(场景2兼容)
+        await downloadFileByBlob(fileUrl, fileName);
+      }
+    }
+  };
+  // 兼容场景2:通过 Blob 流下载(解决跨域或后端返回流的情况)
+  const downloadFileByBlob = async (fileUrl, fileName) => {
+    try {
+      // 1. 发起请求获取文件流(注意:需设置 responseType: 'blob')
+      const response = await fetch(fileUrl, {
+        method: 'GET',
+        headers: {
+          // 若需要登录态,添加 token(根据项目授权方式调整)
+          Authorization: `Bearer ${uni.getStorageSync('token')}`,
+        },
+      });
+
+      if (!response.ok) {
+        throw new Error(`请求失败: ${response.status}`);
+      }
+
+      // 2. 将响应转换为 Blob 对象(根据文件类型设置 MIME,如 PDF 为 'application/pdf')
+      const blob = await response.blob();
+      // 3. 生成 Blob 临时 URL
+      const blobUrl = URL.createObjectURL(blob);
+
+      // 4. 用 <a> 标签触发下载(同场景1逻辑)
+      const safeFileName = decodeURIComponent(fileName || '未命名文件');
+      const link = document.createElement('a');
+      link.download = safeFileName;
+      link.href = blobUrl;
+      link.style.display = 'none';
+      document.body.appendChild(link);
+      link.click();
+
+      // 5. 清理资源
+      setTimeout(() => {
+        document.body.removeChild(link);
+        URL.revokeObjectURL(blobUrl); // 必须释放 Blob URL,避免内存泄漏
+        uni.hideLoading();
+        // uni.showToast({ title: t("operation.downloadSuccess"), icon: "success" });
+      }, 100);
+    } catch (err) {
+      console.error('🚀 ~ Blob 下载失败:', err);
+      uni.hideLoading();
+      // uni.showToast({ title: t("operation.downloadFail"), icon: "error" });
+    }
+  };
+  // -------------------------- 监听 --------------------------
+  // 监听reportData变化
+  watch(
+    () => props.reportData,
+    (newVal, oldVal) => {
+      console.log('监听reportData变化 ~ newVal:', newVal);
+      console.log('🚀监听reportData变化 ~ oldVal:', oldVal);
+      if (newVal?.id) {
+        // 初始化表单数据
+        formDataFormat();
+      }
+    },
+    { deep: true, immediate: true }
+  );
+  // 监听施工工艺变化
+  watch(
+    () => formData.value.techniqueIds,
+    (newVal, oldVal) => {
+      console.log('监听施工工艺变化 ~ newVal:', newVal);
+      console.log('🚀监听施工工艺变化 ~ oldVal:', oldVal);
+      if (newVal.length) {
+        // 根据已选择的施工工艺对应的工作量属性字段
+        getWorkloadInfoByTechnique();
+      } else {
+        // 清空工作量属性字段
+        formData.value.extProperty = [];
+      }
+    },
+    { deep: true, immediate: true }
+  );
+  // -------------------------- 暴露给父组件的外部方法 --------------------------
+  defineExpose({
+    submitForm,
+  });
+  // -------------------------- 事件派发 --------------------------
+  const emit = defineEmits([]);
+</script>
+
+<style lang="scss" scoped>
+  @import '@/style/work-order-form.scss';
+  .report-form {
+    height: 100%;
+    color: #333;
+  }
+  :deep(.uni-textarea-textarea:disabled),
+  :deep(.uni-input-input:disabled) {
+    color: #333;
+  }
+  :deep(.uni-date-x) {
+    color: #333;
+  }
+  .digit-item {
+    text-align: right;
+    :deep(.uni-easyinput__content-input) {
+      padding-right: 10px;
+    }
+  }
+  .time-range-item {
+    margin: 10px;
+  }
+  .form-item-btn {
+    margin: 10px 0 0 0;
+    width: 60px;
+    text-align: center;
+  }
+  .form-item-select {
+    width: 100%;
+  }
+  .slot-box {
+    width: 100%;
+    flex-wrap: wrap;
+  }
+  .slot-content-item {
+    &.selected {
+      background: #f4f4f5;
+      // color: #909399;
+      margin: 5px;
+      padding: 5px;
+      border-radius: 3px;
+    }
+  }
+  .slot-item-text {
+    width: 90%;
+  }
+  .file-picker-container {
+    width: 100%;
+  }
+  .file-picker-btn {
+    margin-left: unset;
+    margin-right: unset;
+  }
+  .file-size-limit {
+    font-size: 10px;
+    color: #ff4500;
+    padding: 0 10px;
+  }
+  .file-list {
+    position: relative;
+    width: 100%;
+    .file-item {
+      position: relative;
+      width: 100%;
+      box-sizing: border-box;
+      padding: 8px 5px;
+      margin-bottom: 10px;
+      border: 1px solid #f5f5f5;
+      // 关键1:允许flex容器换行,适应文字高度
+      flex-wrap: wrap;
+      // 关键2:设置对齐方式,避免换行后按钮错位
+      align-items: flex-start;
+      .file-name {
+        margin-right: 8px;
+        // 关键3:允许文字换行
+        white-space: normal;
+        // 关键4:设置换行规则(中文按字换行,英文按词换行)
+        word-wrap: break-word;
+        word-break: break-all;
+      }
+      .file-btn {
+        color: #004098;
+        min-width: max-content;
+        // 关键5:按钮垂直居中(即使文字换行,按钮也居中)
+        align-self: center;
+      }
+    }
+  }
+</style>

File diff suppressed because it is too large
+ 714 - 213
pages/ruiDu/compontents/report-form.vue


+ 58 - 70
pages/ruiDu/edit.vue

@@ -6,8 +6,7 @@
         :values="tabTitles"
         :style-type="styleType"
         :active-color="activeColor"
-        @clickItem="onClickTabItem"
-      />
+        @clickItem="onClickTabItem" />
     </view>
     <scroll-view scroll-y="true" class="segmented-content">
       <!-- 工单信息 -->
@@ -18,6 +17,9 @@
       <view class="work-order-bom-list" v-show="currentTab === 1">
         <report-form ref="reportFormEditRef" :report-id="reportId" :report-data="detailData" />
       </view>
+      <!-- <view class="work-order-bom-list" v-if="currentTab === 2">
+        <report-form-copy ref="reportFormEditRef" :report-id="reportId" :report-data="detailData" />
+      </view> -->
     </scroll-view>
     <!-- 底部总览 及 操作按钮 -->
     <view class="segmented-footer">
@@ -25,7 +27,7 @@
         <uni-col :span="6">
           <view class="footer-btn">
             <button class="mini-btn" type="primary" @click="save">
-              {{ t("operation.save") }}
+              {{ t('operation.save') }}
             </button>
           </view>
         </uni-col>
@@ -34,76 +36,62 @@
   </view>
 </template>
 <script setup>
-import { onLoad, onReady, onBackPress } from "@dcloudio/uni-app";
-import {
-  reactive,
-  ref,
-  onMounted,
-  onBeforeUnmount,
-  nextTick,
-  getCurrentInstance,
-  watch,
-} from "vue";
-import dayjs from "dayjs";
-// -------------------------- 引入api接口 start--------------------------
-import { getRuiDuReportDetail } from "@/api/ruiDu.js";
-// -------------------------- 引入api接口 end--------------------------
-// --------------------------引入组件----------------------------------
-import reportInfo from "./compontents/report-info.vue";
-import reportForm from "./compontents/report-form.vue";
-// --------------------------引用全局变量$t-------------------------------
-const { appContext } = getCurrentInstance();
-const t = appContext.config.globalProperties.$t;
-// ----------------------------选项卡----------------------------------
-// 选项卡标题
-const tabTitles = ref([t("ruiDu.taskInfo"), t("ruiDu.reportInfo")]);
-const currentTab = ref(0);
-const styleType = ref("text");
-const activeColor = ref("#004098");
-const onClickTabItem = (e) => {
-  currentTab.value = e.currentIndex;
-};
-// --------------------------页面变量----------------------------------
-// 报告ID
-const reportId = ref("");
-// 报告详情数据
-const detailData = ref({});
-// 表单组件ref
-const reportFormEditRef = ref(null);
-// --------------------------生命周期函数----------------------------------
-onLoad((option) => {
-  // 页面加载
-  reportId.value = option.id; // 获取页面参数
+  import { onLoad } from '@dcloudio/uni-app';
+  import { ref, getCurrentInstance } from 'vue';
+  // -------------------------- 引入api接口 start--------------------------
+  import { getRuiDuReportDetail } from '@/api/ruiDu.js';
+  // -------------------------- 引入api接口 end--------------------------
+  // --------------------------引入组件----------------------------------
+  import reportInfo from './compontents/report-info.vue';
+  import reportForm from './compontents/report-form.vue';
+  // --------------------------引用全局变量$t-------------------------------
+  const { appContext } = getCurrentInstance();
+  const t = appContext.config.globalProperties.$t;
+  // ----------------------------选项卡----------------------------------
+  // 选项卡标题
+  const tabTitles = ref([t('ruiDu.taskInfo'), t('ruiDu.reportInfo')]);
+  const currentTab = ref(0);
+  const styleType = ref('text');
+  const activeColor = ref('#004098');
+  const onClickTabItem = e => {
+    currentTab.value = e.currentIndex;
+  };
+  // --------------------------页面变量----------------------------------
+  // 报告ID
+  const reportId = ref('');
+  // 报告详情数据
+  const detailData = ref({});
+  // 表单组件ref
+  const reportFormEditRef = ref(null);
+  // --------------------------生命周期函数----------------------------------
+  onLoad(option => {
+    // 页面加载
+    reportId.value = option.id; // 获取页面参数
+    // 获取日报详情
+    getReportDetail();
+  });
+  // -------------------------- 页面方法 --------------------------
   // 获取日报详情
-  getReportDetail();
-});
-// -------------------------- 页面方法 --------------------------
-// 获取日报详情
-const getReportDetail = () => {
-  getRuiDuReportDetail({ id: reportId.value })
-    .then((res) => {
-      if (res.code === 0) {
-        detailData.value = Object.assign(detailData.value, res.data || {});
-        console.log(
-          "🚀 ~ getReportDetail ~ detailData.value:",
-          detailData.value
-        );
-      }
-    })
-    .catch((res) => {});
-};
-// 保存
-const save = () => {
-  reportFormEditRef.value.submitForm();
-};
+  const getReportDetail = () => {
+    getRuiDuReportDetail({ id: reportId.value })
+      .then(res => {
+        if (res.code === 0) {
+          detailData.value = Object.assign(detailData.value, res.data || {});
+        }
+      })
+      .catch(res => {});
+  };
+  // 保存
+  const save = () => {
+    reportFormEditRef.value.submitForm();
+  };
 
-
-// -------------------------- 页面方法 end --------------------------
+  // -------------------------- 页面方法 end --------------------------
 </script>
 
 <style lang="scss" scoped>
-@import "@/style/work-order-segmented.scss";
-.page {
-  padding-bottom: 0;
-}
+  @import '@/style/work-order-segmented.scss';
+  .page {
+    padding-bottom: 0;
+  }
 </style>

+ 12 - 12
store/modules/dataDict.js

@@ -1,8 +1,8 @@
-import { defineStore } from "pinia";
-import { ref } from "vue";
-import { getAllDataDictList as getList } from "@/api";
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+import { getAllDataDictList as getList } from '@/api';
 
-export const useDataDictStore = defineStore("dataDict", () => {
+export const useDataDictStore = defineStore('dataDict', () => {
   const dataDict = ref([]);
   const map = new Map();
 
@@ -17,11 +17,11 @@ export const useDataDictStore = defineStore("dataDict", () => {
    * 获取type对应字典列表
    * @param type
    */
-  const getDataDictList = (type) => {
+  const getDataDictList = type => {
     if (map.has(type) && map.get(type).length > 0) {
       return map.get(type);
     }
-    const list = dataDict.value.filter((item) => item.dictType === type);
+    const list = dataDict.value.filter(item => item.dictType === type);
     // const locale = uni.getLocale()
     // // label格式为'xxxx~~en**aaaa~~ru**bbbb', 根据当前语言环境进行处理
     // const list = dataDict.value.filter(item => item.dictType === type)
@@ -44,30 +44,30 @@ export const useDataDictStore = defineStore("dataDict", () => {
     return list;
   };
 
-  const getStrDictOptions = (dictType) => {
+  const getStrDictOptions = dictType => {
     // 获得通用的 DictDataType 列表
     const dictOptions = getDataDictList(dictType);
     // 转换成 string 类型的 StringDictDataType 类型
     // why 需要特殊转换:避免 IDEA 在 v-for="dict in getStrDictOptions(...)" 时,el-option 的 key 会告警
     const dictOption = [];
-    dictOptions.forEach((dict) => {
+    dictOptions.forEach(dict => {
       dictOption.push({
         ...dict,
-        value: dict.value + "",
+        value: dict.value + '',
       });
     });
     return dictOption;
   };
-  const getIntDictOptions = (dictType) => {
+  const getIntDictOptions = dictType => {
     // 获得通用的 DictDataType 列表
     const dictOptions = getDataDictList(dictType);
     // 转换成 number 类型的 NumberDictDataType 类型
     // why 需要特殊转换:避免 IDEA 在 v-for="dict in getIntDictOptions(...)" 时,el-option 的 key 会告警
     const dictOption = [];
-    dictOptions.forEach((dict) => {
+    dictOptions.forEach(dict => {
       dictOption.push({
         ...dict,
-        value: parseInt(dict.value + ""),
+        value: parseInt(dict.value + ''),
       });
     });
     return dictOption;

Some files were not shown because too many files changed in this diff