e597b6da4faa1f30c7b3479cdbb96ac5b4fbb0f5..913e7cd145459ca10e80392819aa052454927103
2026-03-18 ZN
feat: 新增销售合同导出功能并增强多个模块交互
913e7c 对比 | 目录
2026-03-18 ZN
feat(productionProcess): 添加工序机台选择功能
2e5e29 对比 | 目录
已修改6个文件
448 ■■■■■ 文件已修改
src/api/salesManagement/salesLedger.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/New.vue 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrder/index.vue 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-top.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesLedger.js
@@ -116,4 +116,16 @@
        method: "get",
        params: query,
    });
}
// 导出销售合同
// /sales/ledger/exportProcessContract/id
export function exportSalesContract(query) {
  console.log(query);
    return request({
        url: "/sales/ledger/exportProcessContract/" + query.id,
        method: "get",
        responseType: "blob",
      });
}
src/views/basicData/customerFile/index.vue
@@ -112,6 +112,14 @@
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="开户银行:"
                          prop="bankName">
              <el-input v-model="form.bankName"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="开户行号:"
                          prop="bankCode">
              <el-input v-model="form.bankCode"
@@ -119,6 +127,8 @@
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="客户分类:"
                          prop="customerType">
@@ -130,6 +140,47 @@
                <el-option label="进销商客户"
                           value="进销商客户" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="法人"
                          prop="corporation">
              <el-input v-model="form.corporation"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="代理人"
                          prop="agent">
              <el-select v-model="form.agent"
                         placeholder="请选择代理人"
                         clearable
                         filterable
                         :disabled="agentOptions.length === 0"
                         :no-data-text="agentNoDataText">
                <el-option v-for="(contact, index) in agentOptions"
                           :key="getAgentOptionKey(contact, index)"
                           :label="getAgentLabel(contact)"
                           :value="contact.contactPhone"
                           :disabled="!contact.contactPhone">
                  <span>{{ contact.contactPerson || "-" }}</span>
                  <span style="float: right; color: var(--el-text-color-secondary); font-size: 12px;">
                    {{ contact.contactPhone || "" }}
                  </span>
                </el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="传真"
                          prop="fax">
              <el-input v-model="form.fax"
                        placeholder="请输入"
                        clearable />
            </el-form-item>
          </el-col>
        </el-row>
@@ -418,12 +469,7 @@
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">银行账号:</span>
                <span class="info-value">{{ detailForm.bankAccount }}</span>
              </div>
            </el-col>
            <el-col :span="12">
              <div class="info-item">
                <span class="info-label">开户行号:</span>
@@ -616,7 +662,7 @@
</template>
<script setup>
  import { onMounted, ref, reactive, getCurrentInstance, toRefs } from "vue";
  import { onMounted, ref, reactive, getCurrentInstance, toRefs, computed, watch } from "vue";
  import { Search, Paperclip, Upload } from "@element-plus/icons-vue";
  import {
    addCustomer,
@@ -690,6 +736,8 @@
    companyPhone: "",
    companyAddress: "",
    basicBankAccount: "",
    corporation: "",
    fax: "",
    bankAccount: "",
    bankCode: "",
    contactPerson: "",
@@ -768,6 +816,16 @@
    {
      label: "开户行号",
      prop: "bankCode",
      width: 220,
    },
    {
      label: "法人代表",
      prop: "corporation",
      width: 220,
    },
    {
      label: "传真",
      prop: "fax",
      width: 220,
    },
    {
@@ -856,6 +914,8 @@
      maintenanceTime: "",
      basicBankAccount: "",
      bankAccount: "",
      fax: "",
      corporation: "",
      bankCode: "",
      customerType: "",
    },
@@ -874,6 +934,9 @@
      ],
      basicBankAccount: [{ required: true, message: "请输入", trigger: "blur" }],
      bankAccount: [{ required: true, message: "请输入", trigger: "blur" }],
      corporation: [{ required: true, message: "请输入法人代表", trigger: "blur" }],
      agent: [{ required: true, message: "请选择代理人", trigger: "change" }],
      bankName: [{ required: true, message: "请输入", trigger: "blur" }],
      bankCode: [{ required: true, message: "请输入", trigger: "blur" }],
      customerType: [{ required: true, message: "请选择", trigger: "change" }],
    },
@@ -934,6 +997,39 @@
    },
  });
  const { searchForm, form, rules } = toRefs(data);
  const agentOptions = computed(() => {
    const list = formYYs.value?.contactList || [];
    return list.filter(item => item && (item.contactPerson || item.contactPhone));
  });
  const agentNoDataText = computed(() => {
    if (agentOptions.value.length === 0) return "请先新增联系人";
    return "无匹配联系人";
  });
  const getAgentLabel = contact => {
    const person = (contact?.contactPerson || "").trim();
    const phone = (contact?.contactPhone || "").trim();
    if (person && phone) return `${person}(${phone})`;
    return person || phone || "-";
  };
  const getAgentOptionKey = (contact, index) => {
    return contact?.contactPhone || contact?.contactPerson || index;
  };
  watch(
    () => agentOptions.value.map(item => item.contactPhone),
    phones => {
      const val = form.value?.agent;
      if (!val) return;
      if (!phones.includes(val)) {
        form.value.agent = "";
      }
    }
  );
  const addNewContact = () => {
    formYYs.value.contactList.push({
      contactPerson: "",
src/views/productionManagement/productionProcess/New.vue
@@ -20,10 +20,40 @@
                message: '最多100个字符',
              }
            ]">
          <el-input v-model="formState.name" />
          <el-input v-model="formState.name" placeholder="请输入工序名称" />
        </el-form-item>
        <el-form-item label="工序编号" prop="no">
          <el-input v-model="formState.no"  />
        <el-form-item
            label="工序机台"
            prop="deviceId"
            :rules="[
                {
                required: true,
                message: '请选择工序类型',
              }
            ]"
        >
          <el-select
              v-model="formState.deviceId"
              placeholder="请选择工序机台"
              filterable
              remote
              clearable
              reserve-keyword
              :remote-method="handleDeviceRemoteSearch"
              :loading="deviceLoading"
              @clear="handleDeviceClear"
              @change="handleDeviceChange"
              @visible-change="handleDeviceDropdownVisible"
              popper-class="device-select-popper"
          >
            <el-option v-for="item in equipmentList" :key="item.id" :label="item.deviceName" :value="item.id" />
            <el-option
                v-if="equipmentList.length > 0 && deviceHasMore"
                :value="__deviceLoadMoreSentinel"
                label="加载更多…"
                disabled
            />
          </el-select>
        </el-form-item>
        <el-form-item
            label="工序类型"
@@ -63,8 +93,9 @@
</template>
<script setup>
import { ref, computed, getCurrentInstance } from "vue";
import { ref, computed, onMounted, getCurrentInstance, reactive, nextTick, onBeforeUnmount } from "vue";
import {add} from "@/api/productionManagement/productionProcess.js";
import {getLedgerPage} from "@/api/equipmentManagement/ledger.js";
const props = defineProps({
  visible: {
@@ -84,6 +115,21 @@
  isQuality: false,
});
// 分页查询参数
const page = reactive({
  current: 1,
  size: 10,
  total: 0,
});
onMounted(() => {
  resetDeviceOptions();
});
const handleDeviceChange = (val) => {
  formState.value.deviceName = equipmentList.value.find(item => item.id === val)?.deviceName || '';
};
const isShow = computed({
  get() {
    return props.visible;
@@ -94,6 +140,112 @@
});
let { proxy } = getCurrentInstance()
const equipmentList = ref([]);
const deviceLoading = ref(false);
const deviceQuery = ref("");
const deviceScrollWrap = ref(null);
const __deviceLoadMoreSentinel = "__deviceLoadMoreSentinel";
const deviceHasMore = computed(() => {
  const total = Number(page.total ?? 0);
  if (!total) {
    return false;
  }
  return equipmentList.value.length < total;
});
// 获取设备列表(分页查询)
const getLedgerPageS = async ({ reset = false } = {}) => {
  if (deviceLoading.value) return;
  deviceLoading.value = true;
  try {
    const res = await getLedgerPage({
      current: page.current,
      size: page.size,
      deviceName: deviceQuery.value ? deviceQuery.value : undefined,
    });
    const data = res?.data || {};
    const records = Array.isArray(data.records) ? data.records : [];
    page.total = Number(data.total ?? page.total ?? 0);
    page.current = Number(data.current ?? page.current);
    page.size = Number(data.size ?? page.size);
    equipmentList.value = reset ? records : [...equipmentList.value, ...records];
  } finally {
    deviceLoading.value = false;
  }
};
const resetDeviceOptions = async () => {
  page.current = 1;
  page.size = 10;
  page.total = 0;
  equipmentList.value = [];
  await getLedgerPageS({ reset: true });
};
const loadMoreDevices = async () => {
  if (deviceLoading.value) return;
  if (!deviceHasMore.value) return;
  page.current += 1;
  await getLedgerPageS();
};
let remoteTimer = null;
const handleDeviceRemoteSearch = (query) => {
  const nextQuery = (query ?? "").trim();
  deviceQuery.value = nextQuery;
  if (remoteTimer) clearTimeout(remoteTimer);
  remoteTimer = setTimeout(() => {
    resetDeviceOptions();
  }, 300);
};
const handleDeviceClear = () => {
  deviceQuery.value = "";
  resetDeviceOptions();
};
const onDeviceDropdownScroll = (e) => {
  const el = e.target;
  if (!el) return;
  if (el.scrollHeight - el.scrollTop - el.clientHeight <= 20) {
    loadMoreDevices();
  }
};
const unbindDeviceDropdownScroll = () => {
  if (deviceScrollWrap.value) {
    deviceScrollWrap.value.removeEventListener("scroll", onDeviceDropdownScroll);
    deviceScrollWrap.value = null;
  }
};
const bindDeviceDropdownScroll = () => {
  unbindDeviceDropdownScroll();
  const wrap =
      document.querySelector(".device-select-popper .el-scrollbar__wrap") ||
      document.querySelector(".device-select-popper .el-select-dropdown__wrap");
  if (wrap) {
    deviceScrollWrap.value = wrap;
    wrap.addEventListener("scroll", onDeviceDropdownScroll);
  }
};
const handleDeviceDropdownVisible = async (visible) => {
  if (!visible) {
    unbindDeviceDropdownScroll();
    return;
  }
  if (equipmentList.value.length === 0) {
    await resetDeviceOptions();
  }
  await nextTick();
  bindDeviceDropdownScroll();
};
const closeModal = () => {
  isShow.value = false;
@@ -118,4 +270,9 @@
  handleSubmit,
  isShow,
});
onBeforeUnmount(() => {
  unbindDeviceDropdownScroll();
  if (remoteTimer) clearTimeout(remoteTimer);
});
</script>
src/views/productionManagement/workOrder/index.vue
@@ -218,13 +218,46 @@
        </span>
      </template>
    </el-dialog>
    <el-dialog
      v-model="auditDialogVisible"
      title="审核"
      width="1000px"
      :close-on-click-modal="false"
    >
      <el-table :data="auditTableData" border style="width: 100%" v-loading="auditLoading">
        <el-table-column label="产品名称" prop="productName" min-width="140" show-overflow-tooltip />
        <el-table-column label="规格" prop="model" min-width="120" show-overflow-tooltip />
        <el-table-column label="单位" prop="unit" width="80" />
        <el-table-column label="工序名称" prop="processName" min-width="120" show-overflow-tooltip />
        <el-table-column label="需求数量" prop="planQuantity" width="110" />
        <el-table-column label="完成数量" prop="completeQuantity" width="110" />
        <el-table-column label="完成进度" prop="completionStatus" width="140">
          <template #default="{ row }">
            <el-progress
              :percentage="toProgressPercentage(row?.completionStatus)"
              :color="progressColor(toProgressPercentage(row?.completionStatus))"
              :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''"
            />
          </template>
        </el-table-column>
        <el-table-column label="计划开始时间" prop="planStartTime" width="140" />
        <el-table-column label="计划结束时间" prop="planEndTime" width="140" />
      </el-table>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary" :loading="auditLoading" @click="submitAudit(1)">通过</el-button>
          <el-button type="danger" :loading="auditLoading" @click="submitAudit(2)">不通过</el-button>
          <el-button :disabled="auditLoading" @click="auditDialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
    <FilesDia ref="workOrderFilesRef" />
  </div>
</template>
<script setup>
  import { onMounted, ref, nextTick } from "vue";
  import { ElMessageBox } from "element-plus";
  import { ElMessageBox, ElMessage } from "element-plus";
  import dayjs from "dayjs";
  import {
    productWorkOrderPage,
@@ -340,6 +373,14 @@
          },
          disabled: row => row.planQuantity <= 0,
        },
        {
          name:"审核",
          color: "#f56c6c",
          clickFun: row => {
            handleAudit(row);
          },
          disabled: row => Number(row?.auditStatus) === 1,
        }
      ],
    },
  ]);
@@ -353,6 +394,10 @@
  const transferCardQrUrl = ref("");
  const transferCardRowData = ref(null);
  const reportDialogVisible = ref(false);
  const auditDialogVisible = ref(false);
  const auditRowData = ref(null);
  const auditTableData = ref([]);
  const auditLoading = ref(false);
  const workOrderFilesRef = ref(null);
  const reportFormRef = ref(null);
  const userOptions = ref([]);
@@ -398,6 +443,62 @@
    callback();
  };
  // 审核
  const handleAudit = (row) => {
    if (Number(row?.auditStatus) === 1) {
      ElMessage.warning("该工单已审核");
      return;
    }
    auditRowData.value = row;
    const workOrderNo = row?.workOrderNo;
    const related = workOrderNo
      ? tableData.value.filter(r => r?.workOrderNo === workOrderNo)
      : [];
    auditTableData.value = related.length > 0 ? related : [row];
    auditDialogVisible.value = true;
  };
  const submitAudit = async (result) => {
    const current = auditRowData.value;
    if (!current) return;
    if (auditLoading.value) return;
    const confirmText = result === 1 ? "确定审核通过吗?" : "确定审核不通过吗?";
    try {
      await ElMessageBox.confirm(confirmText, "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      });
    } catch {
      return;
    }
    auditLoading.value = true;
    try {
      const updates = auditTableData.value.map(item => {
        const id = item?.id;
        if (!id) return Promise.resolve();
        return updateProductWorkOrder({ id, auditStatus: result });
      });
      await Promise.all(updates);
      ElMessage.success("审核成功");
      auditDialogVisible.value = false;
      getList();
    } finally {
      auditLoading.value = false;
    }
  };
  // 查看详情
  const handleView = (row) => {
    const { workOrderId } = row;
    router.push({
      path: "/productionManagement/workOrderDetail",
      query: { workOrderId },
    });
  }
  // 验证规则
  const reportFormRules = {
    quantity: [{ required: true, validator: validateQuantity, trigger: "blur" }],
src/views/reportAnalysis/productionAnalysis/components/center-top.vue
@@ -6,6 +6,7 @@
        v-for="item in statItems"
        :key="item.name"
        class="stat-card"
        @click="handleClick(item)"
      >
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <div class="card-content">
@@ -25,7 +26,11 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { orderCount } from '@/api/viewIndex.js'
const router = useRouter()
const statItems = ref([])
@@ -51,6 +56,16 @@
      console.error('获取订单数量统计失败:', err)
    })
}
const handleClick = (item) => {
  // 点击跳转页面
  console.log('点击了', item)
  router.push({
    path: '/productionManagement/productionOrder',
    query: {
      name: item.name,
    }
  })
}
onMounted(() => {
  fetchData()
@@ -64,6 +79,7 @@
}
.stat-card {
  cursor: pointer;
  flex: 1;
  display: flex;
  align-items: center;
src/views/salesManagement/salesLedger/index.vue
@@ -118,10 +118,11 @@
        <el-table-column label="录入日期" prop="entryDate" width="120" show-overflow-tooltip />
        <el-table-column label="签订日期" prop="executionDate" width="120" show-overflow-tooltip />
        <el-table-column label="交付日期" prop="deliveryDate" width="120" show-overflow-tooltip />
        <el-table-column label="备注" prop="remarks" width="200" show-overflow-tooltip />
        <el-table-column fixed="right" label="操作" min-width="100" align="center">
        <el-table-column label="其它说明事项" prop="remarks" width="200" show-overflow-tooltip />
        <el-table-column fixed="right" label="操作" min-width="200" align="center">
          <template #default="scope">
            <el-button link type="primary" size="small" @click="openForm('edit', scope.row)" :disabled="!scope.row.isEdit">编辑</el-button>
            <el-button link type="primary" size="small" @click="exportSalesContracts(scope.row)">导出销售合同</el-button>
<!--            <el-button link type="primary" size="small" @click="openForm('view', scope.row)">详情</el-button>-->
            <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">附件</el-button>
<!--            <el-button link type="primary" size="small" @click="openDeliveryForm(scope.row)">发货</el-button>-->
@@ -213,6 +214,11 @@
                              type="date" placeholder="请选择" clearable />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="签订地点:" prop="placeOfSinging">
                <el-input v-model="form.placeOfSinging" placeholder="请输入" clearable :disabled="operationType === 'view'" />
            </el-form-item>
          </el-col>
        </el-row>
                <el-row>
                    <el-form-item label="产品信息:" prop="entryDate">
@@ -243,7 +249,7 @@
                </el-table>
                <el-row :gutter="30">
                    <el-col :span="24">
                        <el-form-item label="备注:" prop="remarks">
                        <el-form-item label="其它说明事项:" prop="remarks">
                            <el-input v-model="form.remarks" placeholder="请输入" clearable type="textarea" :rows="2" :disabled="operationType === 'view'" />
                        </el-form-item>
                    </el-col>
@@ -675,6 +681,7 @@
    addOrUpdateSalesLedgerProduct,
    delProduct,
    delLedgerFile, getProductInventory,
    exportSalesContract
} from "@/api/salesManagement/salesLedger.js";
import { modelList, productTreeList } from "@/api/basicData/product.js";
import useFormData from "@/hooks/useFormData.js";
@@ -728,6 +735,7 @@
        entryDate: [{ required: true, message: "请选择", trigger: "change" }],
    deliveryDate: [{ required: true, message: "请选择", trigger: "change" }],
        executionDate: [{ required: true, message: "请选择", trigger: "change" }],
        placeOfSinging: [{ required: true, message: "请输入", trigger: "blur" }],
    },
});
const { form, rules } = toRefs(data);
@@ -2092,6 +2100,34 @@
};
/**
 * 导出销售合同
 *
 * @param row 导出销售合同的相关信息对象
 */
const exportSalesContracts = (row) => {
    exportSalesContract({ id: row.id }).then((res) => {
        if (res) {
            const downloadUrl = window.URL.createObjectURL(res);
      const link = document.createElement('a');
      link.href = downloadUrl;
      console.log(row.executionDate)
      link.download = row.projectName+row.executionDate + "销售合同.docx"; // 设置下载文件名
      link.style.display = 'none'; // 隐藏a标签
      document.body.appendChild(link);
      link.click(); // 触发点击下载
      // 4. 清理资源(避免内存泄漏)
      document.body.removeChild(link);
      window.URL.revokeObjectURL(downloadUrl);
      // 5. 提示导出成功
      proxy.$modal.msgSuccess("导出销售合同成功");
        } else {
            proxy.$modal.msgError(res.msg || "导出销售合同失败");
        }
    });
}
/**
 * 下载文件
 *
 * @param row 下载文件的相关信息对象