feat: 新增销售合同导出功能并增强多个模块交互
- 在销售台账模块添加销售合同导出功能,支持生成Word文档
- 为生产分析中心看板卡片添加点击跳转到生产订单页面的功能
- 在工单管理模块增加审核功能,支持批量审核工单
- 完善客户档案表单,新增开户银行、法人代表、代理人等字段
- 优化销售台账表单,添加签订地点字段并调整标签描述
| | |
| | | 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", |
| | | }); |
| | | } |
| | |
| | | </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" |
| | |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="客户分类:" |
| | | prop="customerType"> |
| | |
| | | <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> |
| | |
| | | </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> |
| | |
| | | </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, |
| | |
| | | companyPhone: "", |
| | | companyAddress: "", |
| | | basicBankAccount: "", |
| | | corporation: "", |
| | | fax: "", |
| | | bankAccount: "", |
| | | bankCode: "", |
| | | contactPerson: "", |
| | |
| | | { |
| | | label: "开户行号", |
| | | prop: "bankCode", |
| | | width: 220, |
| | | }, |
| | | { |
| | | label: "法人代表", |
| | | prop: "corporation", |
| | | width: 220, |
| | | }, |
| | | { |
| | | label: "传真", |
| | | prop: "fax", |
| | | width: 220, |
| | | }, |
| | | { |
| | |
| | | maintenanceTime: "", |
| | | basicBankAccount: "", |
| | | bankAccount: "", |
| | | fax: "", |
| | | corporation: "", |
| | | bankCode: "", |
| | | customerType: "", |
| | | }, |
| | |
| | | ], |
| | | 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" }], |
| | | }, |
| | |
| | | }, |
| | | }); |
| | | 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: "", |
| | |
| | | </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, |
| | |
| | | }, |
| | | disabled: row => row.planQuantity <= 0, |
| | | }, |
| | | { |
| | | name:"审核", |
| | | color: "#f56c6c", |
| | | clickFun: row => { |
| | | handleAudit(row); |
| | | }, |
| | | disabled: row => Number(row?.auditStatus) === 1, |
| | | } |
| | | ], |
| | | }, |
| | | ]); |
| | |
| | | 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([]); |
| | |
| | | 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" }], |
| | |
| | | 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"> |
| | |
| | | |
| | | <script setup> |
| | | import { ref, onMounted } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | import { orderCount } from '@/api/viewIndex.js' |
| | | |
| | | const router = useRouter() |
| | | |
| | | |
| | | const statItems = ref([]) |
| | | |
| | |
| | | console.error('获取订单数量统计失败:', err) |
| | | }) |
| | | } |
| | | const handleClick = (item) => { |
| | | // 点击跳转页面 |
| | | console.log('点击了', item) |
| | | router.push({ |
| | | path: '/productionManagement/productionOrder', |
| | | query: { |
| | | name: item.name, |
| | | } |
| | | }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | fetchData() |
| | |
| | | } |
| | | |
| | | .stat-card { |
| | | cursor: pointer; |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | |
| | | <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>--> |
| | |
| | | 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"> |
| | |
| | | </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> |
| | |
| | | addOrUpdateSalesLedgerProduct, |
| | | delProduct, |
| | | delLedgerFile, getProductInventory, |
| | | exportSalesContract |
| | | } from "@/api/salesManagement/salesLedger.js"; |
| | | import { modelList, productTreeList } from "@/api/basicData/product.js"; |
| | | import useFormData from "@/hooks/useFormData.js"; |
| | |
| | | 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); |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 导出销售合同 |
| | | * |
| | | * @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 下载文件的相关信息对象 |