张诺
3 小时以前 5cfebd7e46c0c53f79b5fb4a917e926194ab4398
feat(生产订单): 完善绑定工艺路线弹窗功能及文件预览组件

- 在绑定工艺路线弹窗中新增查看模式,支持只读展示已绑定的工艺路线详情
- 修复文件上传组件中URL解析逻辑,支持更多格式的文件地址处理
- 为文件预览组件增加openUrl方法,支持直接传入完整URL进行预览
- 完善绑定工艺路线的提交逻辑,增加表单验证和成功/失败提示
- 优化弹窗打开逻辑,修复数据加载和表单重置的问题
- 在文件操作函数中添加调试日志,便于问题排查
已修改5个文件
488 ■■■■ 文件已修改
src/components/Upload/ActionFileUpload.vue 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/filePreview/index.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/knowledgeBase/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/BindRouteDialog.vue 381 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Upload/ActionFileUpload.vue
@@ -7,14 +7,17 @@
    ref="fileUploadRef"
    :auto-upload="autoUpload"
    :headers="headers"
    :limit="limit"
    :disabled="disabled"
    :before-upload="handleBeforeUpload"
    :on-exceed="handleExceed"
    :on-error="handleUploadError"
    :on-success="handleUploadSuccess"
    :on-remove="handleRemove"
    :on-preview="handlePreview"
    :show-file-list="showFileList"
  >
    <el-button type="primary">{{ buttonText }}</el-button>
    <el-button v-if="!disabled" type="primary">{{ buttonText }}</el-button>
    <template #file="{ file }">
      <div style="display:flex; align-items:center; gap: 10px; width: 100%;">
        <span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
@@ -23,7 +26,7 @@
        <div style="display:flex; align-items:center; gap: 6px;">
          <el-button link type="success" :icon="Download" @click="handleDownload(file)" />
          <el-button link type="primary" :icon="View" @click="handlePreview(file)" />
          <el-button link type="danger" :icon="Delete" @click="triggerRemoveFile(file)" />
          <el-button link type="danger" :icon="Delete" @click="triggerRemoveFile(file)" v-if="onView" />
        </div>
      </div>
    </template>
@@ -60,6 +63,14 @@
    type: Boolean,
    default: true,
  },
  limit: {
    type: Number,
    default: undefined,
  },
  replaceOnExceed: {
    type: Boolean,
    default: false,
  },
  autoUpload: {
    type: Boolean,
    default: true,
@@ -67,6 +78,10 @@
  showFileList: {
    type: Boolean,
    default: true,
  },
  disabled: {
    type: Boolean,
    default: false,
  },
  buttonText: {
    type: String,
@@ -100,6 +115,10 @@
    type: Function,
    default: null,
  },
  onView: {
    type: Boolean,
    default: true,
  },
});
const emit = defineEmits([
@@ -118,8 +137,27 @@
  set: (val) => emit("update:fileList", val),
});
const resolveUrl = (url) => {
  const u = String(url || "");
  if (!u) return "";
  if (/^(https?:)?\/\//i.test(u)) return u;
  if (/^(blob:|data:)/i.test(u)) return u;
  const baseUrl = import.meta.env.VITE_APP_BASE_API || "";
  if (!baseUrl) return u;
  if (u.startsWith("/")) return baseUrl + u;
  return baseUrl + "/" + u;
};
const getFileUrl = (file) => {
  return file?.url || file?.response?.data?.tempPath || file?.response?.data?.url || "";
  const respData = file?.response?.data;
  const rawUrl =
    file?.url ||
    (Array.isArray(respData) ? respData?.[0]?.fileUrl : undefined) ||
    respData?.fileUrl ||
    respData?.tempPath ||
    respData?.url ||
    "";
  return resolveUrl(rawUrl);
};
const triggerRemoveFile = (file) => {
@@ -138,7 +176,31 @@
  emit("error", ...args);
};
const handleExceed = (files) => {
  if (!props.replaceOnExceed) return;
  if (props.limit !== 1) return;
  const file = files?.[0];
  if (!file) return;
  fileUploadRef.value?.clearFiles?.();
  innerFileList.value = [];
  fileUploadRef.value?.handleStart?.(file);
  if (props.autoUpload) {
    fileUploadRef.value?.submit?.();
  }
};
const handleUploadSuccess = (...args) => {
  const [response, uploadFile, uploadFiles] = args;
  if (uploadFile && !uploadFile.url) {
    const rawUrl = response?.data?.tempPath || response?.data?.url || response?.url || "";
    const resolvedUrl = resolveUrl(rawUrl);
    if (resolvedUrl) {
      uploadFile.url = resolvedUrl;
    }
  }
  if (props.limit === 1 && Array.isArray(uploadFiles) && uploadFiles.length) {
    innerFileList.value = [uploadFiles[uploadFiles.length - 1]];
  }
  props.onSuccess?.(...args);
  emit("success", ...args);
};
src/components/filePreview/index.vue
@@ -88,7 +88,6 @@
});
const isPdf = computed(() => {
  console.log(fileUrl.value)
  return /\.pdf$/i.test(fileUrl.value);
});
@@ -167,6 +166,11 @@
  fileUrl.value = window.location.protocol+'//'+window.location.host+ url;
  dialogVisible.value = true;
};
const openUrl = (url) => {
  fileUrl.value = url;
  dialogVisible.value = true;
}
const handleClose = () => {
  dialogVisible.value = false;
};
@@ -183,7 +187,8 @@
// 暴露open方法供外部调用
defineExpose({
  open
  open,
  openUrl
})
</script>
src/views/collaborativeApproval/knowledgeBase/index.vue
@@ -767,6 +767,7 @@
// 文件预览/下载
const handleDownload = (file) => {
  console.log(file)
  const url = getUploadFileUrl(file)
  if (!url) return
  proxy?.$modal?.loading?.("正在下载文件,请稍候...")
@@ -775,6 +776,7 @@
}
function handlePreview(file) {
  console.log(file)
  const url = getUploadFileUrl(file)
  if (!url) return
  filePreviewRef.value?.open?.(url)
src/views/productionManagement/productionOrder/BindRouteDialog.vue
@@ -1,7 +1,7 @@
<template>
  <FormDialog
    v-model="visible"
    :title="type === 'add' ? '绑定工艺路线' : '编辑工艺路线'"
    :title="type === 'add' ? '绑定工艺路线' : type === 'detail' ? '查看工艺路线' : '编辑工艺路线'"
    width="1400px"
    :operation-type="type"
    :column="8"
@@ -9,6 +9,7 @@
    @confirm="handleConfirm"
    @cancel="handleClose"
  >
    <div :class="{ 'is-detail': isDetail }">
    <!-- ================= 基本信息 ================= -->
    <el-descriptions :column="3">
      <el-descriptions-item label="编号" align="center" v-if="formData.productOrderList">
@@ -34,7 +35,7 @@
      </el-descriptions-item>
      <el-descriptions-item label="成品尺寸" :span="1" align="center">
        {{formData.specificationModel || "--"}}
        {{formData.finishedSize || "--"}}
      </el-descriptions-item>
      <el-descriptions-item label="产品名称" :span="2" align="center">
@@ -42,7 +43,7 @@
      </el-descriptions-item>
      <el-descriptions-item label="单据类型" :span="2" align="center">
        <el-checkbox-group v-model="introductionLetterList">
        <el-checkbox-group v-model="introductionLetterList" :disabled="isDetail">
          <el-checkbox label="介绍信" value="介绍信" />
          <el-checkbox label="商标注册书" value="商标注册书" />
          <el-checkbox label="委印单" value="委印单" />
@@ -53,12 +54,13 @@
    <!-- ================= 材料表 ================= -->
    <div class="process-table-header">
     <div class="section-title">材料信息</div>
      <el-button type="primary" size="small" @click="addMaterialRow">新增一行</el-button>
      <el-button v-if="!isDetail" type="primary" size="small" @click="addMaterialRow">新增一行</el-button>
    </div>
    <el-table border :data="formData.materialInfo" style="width: 100%">
      <el-table-column label="材料名称">
        <template #default="{ row }">
        <template #default="{ row }" >
          <el-tree-select
          v-if="!isDetail"
            v-model="row.productId"
            placeholder="请选择"
            clearable
@@ -66,18 +68,22 @@
            @change="(val) => getModels(val, row)"
            :data="productOptions"
            :render-after-expand="false"
            :disabled="isDetail"
            style="width: 100%"
          />
        </template>
            <span v-else>{{ row.name }}</span>
        </template>
      </el-table-column>
      <el-table-column label="规格">
        <template #default="{ row }">
          <el-select
          v-if="!isDetail"
            v-model="row.productModelId"
            placeholder="请选择规格"
            filterable
            clearable
            @change="(val) => handleMaterialModelChange(val, row)"
            :disabled="isDetail"
          >
            <el-option
              v-for="item in row.modelOptions || []"
@@ -86,33 +92,32 @@
              :value="item.id"
            />
          </el-select>
          <span v-else>{{ row.model }}</span>
        </template>
      </el-table-column>
      <el-table-column label="数量">
        <template #default="{ row }">
          <el-input v-model="row.num" placeholder="数量">
            <template #append>{{ row.numSuffix }}</template>
          <el-input v-model="row.num" placeholder="数量" :disabled="isDetail">
          </el-input>
        </template>
      </el-table-column>
      <el-table-column label="计量单位">
        <template #default="{ row }">
          <el-input v-model="row.unit" placeholder="计量单位" />
          <el-input v-model="row.unit" placeholder="计量单位" :disabled="isDetail" />
        </template>
      </el-table-column>
      <el-table-column label="单价">
        <template #default="{ row }">
          <el-input v-model="row.price" placeholder="单价">
            <template #append>{{ row.unitSuffix }}</template>
          <el-input v-model="row.price" placeholder="单价" :disabled="isDetail">
          </el-input>
        </template>
      </el-table-column>
      <el-table-column label="金额">
        <template #default="{ row }">
          <el-input v-model="row.totalAmount" placeholder="金额" />
          <el-input v-model="row.totalAmount" placeholder="金额" :disabled="isDetail" />
        </template>
      </el-table-column>
      <el-table-column label="操作" width="80">
      <el-table-column v-if="!isDetail" label="操作" width="80">
        <template #default="{ $index }">
          <el-button type="danger" size="small" @click="removeMaterialRow($index)">删除</el-button>
        </template>
@@ -130,20 +135,34 @@
            :autosize="{ minRows: 2, maxRows: 4 }"
            type="textarea"
            placeholder="请输入注意事项"
            :disabled="isDetail"
        />
      </el-descriptions-item>
    </el-descriptions>
    <hr>
    <!-- ================= 切料图示 ================= -->
    <div class="section-title">切料图示</div>
    <ActionFileUpload
        style="width: 50%;"
        v-model:file-list="fileList"
        style="width: 50%;float: left;"
        v-model:file-list="formData.cuttingFileVo"
        :action="upload.url"
        :headers="upload.headers"
        :multiple="false"
        :limit="1"
        :replaceOnExceed="true"
        :disabled="isDetail"
        :name="'files'"
    tip-text="支持图片(jpg, jpeg, png)格式"
        :onSuccess="onSuccess"
        :onDownload="onDownload"
        :onRemove="onRemove"
        :onPreview="onPreview"
        :onView="type === 'detail' ? false : true"
        :tip-text="type === 'detail' ? '' : '支持图片(jpg, jpeg, png)格式'"
    />
    <el-image
      v-if="formData.cuttingFileVo.length > 0"
      style="width: 100px; height: 100px"
      :src="resolveFileUrl(getUploadFileUrl(formData.cuttingFileVo[0]))"
      fit="cover"
    />
    <!-- ================= 切料信息 ================= -->
    <el-descriptions
@@ -154,22 +173,22 @@
      class="fixed-desc"
    >
      <el-descriptions-item label="切料尺寸" align="center">
        <el-input v-model="formData.cutNum" placeholder="切料尺寸" />
        <el-input v-model="formData.cutNum" placeholder="切料尺寸" :disabled="isDetail" />
      </el-descriptions-item>
      <el-descriptions-item label="切料数量" align="center">
        <el-input v-model="formData.cutSize" placeholder="切料尺寸" />
        <el-input v-model="formData.cutSize" placeholder="切料尺寸" :disabled="isDetail" />
      </el-descriptions-item>
      <el-descriptions-item label="中盒数量" align="center">
        <el-input v-model="formData.mediumBoxQty" placeholder="中盒数量" />
        <el-input v-model="formData.mediumBoxQty" placeholder="中盒数量" :disabled="isDetail" />
      </el-descriptions-item>
      <el-descriptions-item label="小盒数量" align="center">
        <el-input v-model="formData.smallBoxQty" placeholder="小盒数量" />
        <el-input v-model="formData.smallBoxQty" placeholder="小盒数量" :disabled="isDetail" />
      </el-descriptions-item>
      <el-descriptions-item label="正数" align="center">
        <el-input v-model="formData.positiveQty" placeholder="正数" />
        <el-input v-model="formData.positiveQty" placeholder="正数" :disabled="isDetail" />
      </el-descriptions-item>
      <el-descriptions-item label="加放数" align="center">
        <el-input v-model="formData.allowanceQty" placeholder="加放数" />
        <el-input v-model="formData.allowanceQty" placeholder="加放数" :disabled="isDetail" />
      </el-descriptions-item>
    </el-descriptions>
@@ -196,25 +215,25 @@
          </tr>
          <tr v-for="(plate, index) in formData.plateMaking" :key="index">
            <td>
              <el-input v-model="plate.designProductionFee" placeholder="请输入设计制作费" />
              <el-input v-model="plate.designProductionFee" placeholder="请输入设计制作费" :disabled="isDetail" />
            </td>
            <td>
              <el-input v-model="plate.impositionFee" placeholder="请输入拼版费" />
              <el-input v-model="plate.impositionFee" placeholder="请输入拼版费" :disabled="isDetail" />
            </td>
            <td>
              <el-input v-model="plate.filmOutputFee" placeholder="请输入出片费" />
              <el-input v-model="plate.filmOutputFee" placeholder="请输入出片费" :disabled="isDetail" />
            </td>
            <td>
              <el-input v-model="plate.proofingFee" placeholder="请输入打样费" />
              <el-input v-model="plate.proofingFee" placeholder="请输入打样费" :disabled="isDetail" />
            </td>
            <td>
              <el-input v-model="plate.doctorBladePlateFee" placeholder="请输入别刀版费" />
              <el-input v-model="plate.doctorBladePlateFee" placeholder="请输入别刀版费" :disabled="isDetail" />
            </td>
            <td>
              <el-input v-model="plate.hotEmbossingPlateFee" placeholder="请输入烫/凸版费" />
              <el-input v-model="plate.hotEmbossingPlateFee" placeholder="请输入烫/凸版费" :disabled="isDetail" />
            </td>
            <td>
              <el-input v-model="plate.subtotalFee" placeholder="请输入小计" />
              <el-input v-model="plate.subtotalFee" placeholder="请输入小计" :disabled="isDetail" />
            </td>
          </tr>
        </tbody>
@@ -224,17 +243,16 @@
    <!-- ================= 工艺加工 ================= -->
    <div class="process-table-header">
     <div class="section-title">工艺加工</div>
      <el-button type="primary" size="small" @click="addProcessRow">新增一行</el-button>
      <el-button v-if="!isDetail" type="primary" size="small" @click="addProcessRow">新增一行</el-button>
    </div>
    <el-table border :data="formData.processContent" style="width: 100%" :span-method="objectSpanMethod">
      <el-table-column label="工序" width="140">
        <template #default="{ row }">
          <el-table-column label="工序" width="140">
            <template #default="{ row }">
              <el-select
                  v-model="row.processId"
                  placeholder="请选择工序"
                  @change="(val) => onProcessChange(val, row)"
                  :disabled="isDetail"
              >
                <el-option
                    v-for="item in processOptions"
@@ -245,24 +263,25 @@
              </el-select>
            </template>
          </el-table-column>
        </template>
      </el-table-column>
      <el-table-column label="开数">
        <template #default="{ row }">
          <el-input v-model="row.openCount" placeholder="请输入开数" />
          <el-input v-model="row.openCount" placeholder="请输入开数" :disabled="isDetail" />
        </template>
      </el-table-column>
      <el-table-column label="工艺正数">
        <template #default="{ row }">
          <el-input v-model="row.processPositive" placeholder="请输入工艺正数" />
          <el-input v-model="row.processPositive" placeholder="请输入工艺正数" :disabled="isDetail" />
        </template>
      </el-table-column>
      <el-table-column label="加放数">
        <template #default="{ row }">
          <el-input v-model="row.allowanceQty" placeholder="请输入加放数" />
          <el-input v-model="row.allowanceQty" placeholder="请输入加放数" :disabled="isDetail" />
        </template>
      </el-table-column>
      <el-table-column label="机台" width="180">
      <el-table-column width="180">
        <template #header>
          <span class="required">*</span>机台
        </template>
        <template #default="{ row }">
          <el-select
            v-model="row.deviceId"
@@ -270,6 +289,7 @@
            filterable
            clearable
            @change="(val) => handleDeviceChange(val, row)"
            :disabled="isDetail"
          >
            <el-option
              v-for="item in deviceOptions"
@@ -280,9 +300,13 @@
          </el-select>
        </template>
      </el-table-column>
      <el-table-column label="报工人" width="220">
      <el-table-column width="220">
        <template #header>
          <span class="required">*</span>报工人
        </template>
        <template #default="{ row }">
          <el-select
          v-if="!isDetail"
            v-model="row.reportUserIds"
            placeholder="请选择报工人"
            filterable
@@ -291,6 +315,7 @@
            collapse-tags
            collapse-tags-tooltip
            @change="(val) => handleReportUsersChange(val, row)"
            :disabled="isDetail"
          >
            <el-option
              v-for="item in userOptions"
@@ -299,6 +324,14 @@
              :value="item.userId"
            />
          </el-select>
  <el-tag
  v-else
    v-for="item in row.reportWorkerList"
    :key="item.id"
  >
    {{ item.userName }}
  </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="工艺要求">
@@ -308,10 +341,11 @@
            type="textarea"
            :rows="6"
            placeholder="请输入工艺要求"
            :disabled="isDetail"
          />
        </template>
      </el-table-column>
      <el-table-column label="操作" width="80">
      <el-table-column v-if="!isDetail" label="操作" width="80">
        <template #default="{ $index }">
          <el-button type="danger" size="small" @click="removeProcessRow($index)">删除</el-button>
        </template>
@@ -321,19 +355,19 @@
    <!-- ================= 包装信息 ================= -->
    <el-descriptions border :column="3" class="mt">
      <el-descriptions-item label="送货地点" align="center">
        <el-input v-model="formData.deliveryAddress" placeholder="送货地点" />
        <el-input v-model="formData.deliveryAddress" placeholder="送货地点" :disabled="isDetail" />
      </el-descriptions-item>
      <el-descriptions-item label="联系人" align="center">
        <el-input v-model="formData.contactName" placeholder="联系人" />
        <el-input v-model="formData.contactName" placeholder="联系人" :disabled="isDetail" />
      </el-descriptions-item>
      <el-descriptions-item label="包装要求" align="center">
        <el-input v-model="formData.packagingRequirement" placeholder="包装要求" />
        <el-input v-model="formData.packagingRequirement" placeholder="包装要求" :disabled="isDetail" />
      </el-descriptions-item>
      <el-descriptions-item label="尺寸" align="center">
        <el-input v-model="formData.postProcessSize" placeholder="尺寸" />
        <el-input v-model="formData.postProcessSize" placeholder="尺寸" :disabled="isDetail" />
      </el-descriptions-item>
      <el-descriptions-item label="定货数量" align="center">
@@ -341,14 +375,16 @@
      </el-descriptions-item>
      <el-descriptions-item label="实交数量" :span="3" align="center">
        <el-input v-model="formData.actualDeliveryQty" placeholder="实交数量" />
        <el-input v-model="formData.actualDeliveryQty" placeholder="实交数量" :disabled="isDetail" />
      </el-descriptions-item>
    </el-descriptions>
    </div>
  </FormDialog>
  <filePreview ref="filePreviewRef" />
</template>
<script setup>
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { ref, reactive, computed, onMounted, watch, getCurrentInstance } from 'vue'
import dayjs from 'dayjs'
import FormDialog from '@/components/Dialog/FormDialog.vue'
import ActionFileUpload from "@/components/Upload/ActionFileUpload.vue";
@@ -358,6 +394,8 @@
import { getDeviceLedger } from "@/api/equipmentManagement/ledger.js"
import { userListNoPageByTenantId } from "@/api/system/user.js"
import { getToken } from "@/utils/auth";
import filePreview from '@/components/filePreview/index.vue'
import { ElMessage } from "element-plus";
const props = defineProps({
  modelValue: {
@@ -379,30 +417,31 @@
})
const emit = defineEmits(['update:modelValue', 'confirm'])
const { proxy } = getCurrentInstance()
const visible = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val)
})
const isDetail = computed(() => props.type === 'detail')
const processOptions = ref([])
const deviceOptions = ref([])
const userOptions = ref([])
const reportWorkerList = ref([])
const productOptions = ref([])
const introductionLetterList = ref([])
const fileList = ref([])
const upload = reactive({
  url: import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload',
  headers: { Authorization: 'Bearer ' + getToken() }
})
const filePreviewRef = ref()
const formData = reactive({
  productOrderList:null,
  salesLedgerId: null,
  productOrderId: null,
  printOrderTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
  fileList:[],
  cuttingFileVo:[],
  finishTime: "",
  no: "",
  productName: "",
@@ -473,16 +512,158 @@
  specificationModel:"",
})
const getUploadFileUrl = (file) => {
  console.log("file", file)
  const response = file?.response
  const data = response?.data
  if (Array.isArray(data) && data.length) {
    return data[0]?.fileUrl || data[0]?.url || data[0]?.tempPath || ""
  }
  return file?.url || file?.fileUrl || data?.tempPath || data?.url || data?.fileUrl || ""
}
// 监听 checkbox group 变化并同步到 introductionLetter 字符串
watch(introductionLetterList, (val) => {
  formData.introductionLetter = val.join(',')
})
const onProcessChange = (processId, row) => {
  const selected = processOptions.find(item => item.id === processId)
  const selected = processOptions.value.find(item => item.id === processId)
  row.processName = selected?.name || ''
}
const cloneDeep = (val) => JSON.parse(JSON.stringify(val))
const createDefaultFormData = () => ({
  productOrderList: null,
  salesLedgerId: null,
  productOrderId: null,
  printOrderTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
  cuttingFileVo: [],
  finishTime: "",
  no: "",
  productName: "",
  productDescription: "",
  clientName: "",
  finishedSize: "",
  cutNum: "",
  cutSize: "",
  mediumBoxQty: "",
  smallBoxQty: "",
  positiveQty: "",
  allowanceQty: "",
  introductionLetter: "",
  plateMaking: [
    {
      designProductionFee: "",
      impositionFee: "",
      filmOutputFee: "",
      proofingFee: "",
      doctorBladePlateFee: "",
      hotEmbossingPlateFee: "",
      subtotalFee: ""
    }
  ],
  processContent: [
    {
      id: "1",
      processId: "",
      processName: "",
      mediumBoxQty: "",
      smallBoxQty: "",
      openCount: "",
      processPositive: "",
      allowanceQty: "",
      deviceId: "",
      deviceName: "",
      reportUserIds: [],
      reportWorkerList: []
    }
  ],
  materialInfo: [
    {
      id: "1",
      productId: "",
      name: "",
      productModelId: "",
      model: "",
      modelOptions: [],
      num: "",
      numSuffix: "张",
      unitSuffix: "元/kg",
      unit: "",
      price: "",
      totalAmount: ""
    }
  ],
  processRequirement: "",
  deliveryAddress: "",
  contactName: "",
  packagingRequirement: "",
  postProcessSize: "",
  orderQty: "",
  actualDeliveryQty: "",
  productionDept: "",
  technicalDept: "",
  warehouseDept: "",
  productModelId: "",
  specificationModel: "",
  cuttingFileId:""
})
const resetForm = () => {
  const next = createDefaultFormData()
  Object.keys(next).forEach((key) => {
    formData[key] = Array.isArray(next[key]) ? cloneDeep(next[key]) : next[key]
  })
  introductionLetterList.value = []
}
const onSuccess = (response, uploadFile, uploadFiles) => {
    const data = response?.data
  if (uploadFile && !uploadFile.url) {
    uploadFile.url = (Array.isArray(data) ? data?.[0]?.fileUrl : data?.fileUrl) || data?.tempPath || data?.url || response?.url || ""
  }
  if (Array.isArray(uploadFiles) && uploadFiles.length) {
    formData.cuttingFileVo = [uploadFiles[uploadFiles.length - 1]]
    formData.cuttingFileId = data?.[0]?.id || ""
  }
}
const JavaApi = computed(() => proxy?.javaApi || "")
const onRemove = (file) => {
  formData.cuttingFileVo = []
  formData.cuttingFileId = ""
}
const resolveFileUrl = (rawUrl) => {
  console.log("rawUrl", rawUrl)
  const u = String(rawUrl || "")
  if (!u) return ""
  if (/^(https?:)?\/\//i.test(u)) return u
  if (/^(blob:|data:)/i.test(u)) return u
  const base = String(JavaApi.value || "").replace(/\/+$/, "")
  if (!base) return u
  if (u.startsWith("/")) return base + u
  return base + "/" + u
}
// 文件预览/下载
const onDownload = (file) => {
  console.log(file)
  const url = resolveFileUrl(getUploadFileUrl(file))
  if (!url) return
  proxy?.$modal?.loading?.("正在下载文件,请稍候...")
  proxy.$download.name(url);
  proxy?.$modal?.closeLoading?.()
}
const onPreview = (file) => {
  const url = resolveFileUrl(getUploadFileUrl(file))
  if (!url) return
  filePreviewRef.value?.openUrl?.(url);
}
const mergeRowDataToForm = (source) => {
@@ -495,6 +676,19 @@
      formData[key] = Array.isArray(source[key]) ? cloneDeep(source[key]) : source[key]
    }
  })
  if (formData.cuttingFileVo && !Array.isArray(formData.cuttingFileVo)) {
    formData.cuttingFileVo = [formData.cuttingFileVo]
  }
  if (Array.isArray(formData.cuttingFileVo)) {
    formData.cuttingFileVo = formData.cuttingFileVo
      .filter(Boolean)
      .map((f) => ({
        ...f,
        name: f?.name || f?.fileName || "",
        url: f?.url || f?.fileUrl || "",
      }))
  }
  // 兼容 index.vue 里常用字段名与弹窗字段名不一致的情况
  if (source.productName === undefined && source.productCategory !== undefined) {
@@ -512,6 +706,11 @@
  if (source.productOrderId === undefined && source.id !== undefined) {
    formData.productOrderId = source.id
  }
  introductionLetterList.value = String(formData.introductionLetter || "")
    .split(",")
    .map(s => s.trim())
    .filter(Boolean)
}
// 获取销售订单
@@ -704,7 +903,30 @@
}
const handleConfirm = () => {
  emit('confirm', JSON.parse(JSON.stringify(formData)))
  if (isDetail.value) {
    return
  }
  const rows = Array.isArray(formData.processContent) ? formData.processContent : []
  for (let i = 0; i < rows.length; i++) {
    const row = rows[i] || {}
    if (!row.deviceId) {
      ElMessage.warning(`工艺加工第${i + 1}行:机台必填`)
      return
    }
    if (!Array.isArray(row.reportUserIds) || row.reportUserIds.length === 0) {
      ElMessage.warning(`工艺加工第${i + 1}行:报工人必填`)
      return
    }
  }
  const payload = cloneDeep(formData)
  delete payload.productOrderList
  if (Array.isArray(payload.materialInfo)) {
    payload.materialInfo = payload.materialInfo.map((item) => {
      const { modelOptions, ...rest } = item || {}
      return rest
    })
  }
  emit('confirm', payload)
}
onMounted(() => {
@@ -714,7 +936,8 @@
  getMaterialProductOptions()
})
defineExpose({
  getProductOrder
  getProductOrder,
  resetForm
})
</script>
@@ -784,7 +1007,59 @@
.mt {
  margin-top: 20px;
}
:deep(.required) {
  color: #f56c6c;
}
:deep(.el-textarea__inner){
  box-shadow: none;
}
.is-detail {
  :deep(.el-input__wrapper) {
    box-shadow: none !important;
    background-color: transparent !important;
  }
  :deep(.el-input__inner) {
    color: var(--el-text-color-regular) !important;
    -webkit-text-fill-color: var(--el-text-color-regular) !important;
    text-overflow: ellipsis;
    white-space: nowrap;
    overflow: hidden;
  }
  :deep(.el-input-group__append),
  :deep(.el-input-group__prepend) {
    background-color: transparent !important;
    box-shadow: none !important;
    color: var(--el-text-color-regular) !important;
  }
  :deep(.el-select__wrapper) {
    box-shadow: none !important;
    background-color: transparent !important;
  }
  :deep(.el-select__selected-item) {
    color: var(--el-text-color-regular) !important;
    text-overflow: ellipsis;
    white-space: nowrap;
    overflow: hidden;
  }
  :deep(.el-select__caret) {
    display: none;
  }
  :deep(.el-textarea__inner) {
    box-shadow: none !important;
    background-color: transparent !important;
    color: var(--el-text-color-regular) !important;
    -webkit-text-fill-color: var(--el-text-color-regular) !important;
    overflow: hidden;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
  }
}
</style>
src/views/productionManagement/productionOrder/index.vue
@@ -110,7 +110,13 @@
  const handleBindRouteSubmit =async (data)=>{
    const res = await saveProductionProductInput(data)
    console.log(res)
    if(res.code === 200){
      proxy.$modal.msgSuccess("绑定成功");
      bindRouteDialogVisible.value = false
      handleQuery()
    }else{
      proxy.$modal.msgError(res.msg || "绑定失败")
    }
  }
@@ -279,18 +285,20 @@
  const openBindRouteDialog = async (row,type) => {
    bindForm.orderId = row.id;
    bindForm.routeId = null;
    bindRouteDialogVisible.value = true;
    routeOptions.value = [];
    bindRouteLoading.value = true;
    if(type === "view") {
      bindDialogType.value = "view"
      let res = await viewGetByProductWordId(row.id)
      console.log(res)
    }
    BindRouteDialogRef.value?.getProductOrder()
    try {
      rowData.value = row;
      BindRouteDialogRef.value?.resetForm?.()
      if (type === "view") {
        bindDialogType.value = "detail"
        const res = await viewGetByProductWordId(row.id)
        rowData.value = res?.data || res
      } else {
        bindDialogType.value = "add"
        rowData.value = row
        rowData.value.finishedSize = row.specificationModel
      }
      bindRouteDialogVisible.value = true;
    } catch (e) {
      console.error("获取工艺路线列表失败:", e);
      proxy.$modal.msgError("获取工艺路线列表失败");