| | |
| | | </div> |
| | | <div class="table_list"> |
| | | <div class="actions"> |
| | | <div></div> |
| | | <div> |
| | | <el-button type="primary" @click="openForm('add')"> |
| | | 新增台账 |
| | | </el-button> |
| | | <el-button type="primary" plain @click="handleImport">导入</el-button> |
| | | <el-button @click="handleOut">导出</el-button> |
| | | <el-button type="danger" plain @click="handleDelete">删除</el-button> |
| | | <el-button type="primary" plain @click="handlePrint">打印</el-button> |
| | | <OtherAmountMaintenanceButton /> |
| | | <ProcessFlowMaintenanceButton /> |
| | | </div> |
| | | <ProcessFlowConfigSelectDialog |
| | | v-model:visible="processFlowSelectDialogVisible" |
| | | :default-route-id="processFlowSelectDefaultRouteId" |
| | | :bound-route-name="processFlowSelectBoundRouteName" |
| | | @confirm="handleProcessFlowSelectConfirm" |
| | | /> |
| | | <el-space wrap> |
| | | <el-button type="primary" @click="openForm('add')">新增台账</el-button> |
| | | <el-button type="primary" @click="handleBulkDelivery">发货</el-button> |
| | | <el-button type="primary" plain @click="handleImport">导入</el-button> |
| | | <el-button @click="handleOut">导出</el-button> |
| | | |
| | | <el-button type="danger" plain @click="handleDelete">删除</el-button> |
| | | |
| | | <el-dropdown @command="handlePrintCommand"> |
| | | <el-button type="primary" plain> |
| | | 打印单据<el-icon class="el-icon--right"> |
| | | <ArrowDown /> |
| | | </el-icon> |
| | | </el-button> |
| | | <template #dropdown> |
| | | <el-dropdown-menu> |
| | | <el-dropdown-item command="finishedProcessCard">生产流程卡(成品)</el-dropdown-item> |
| | | <el-dropdown-item command="salesOrder">销售订单</el-dropdown-item> |
| | | <el-dropdown-item command="salesDeliveryNote">销售发货单</el-dropdown-item> |
| | | </el-dropdown-menu> |
| | | </template> |
| | | </el-dropdown> |
| | | <el-button type="primary" plain @click="handlePrintLabel">打印标签</el-button> |
| | | </el-space> |
| | | </div> |
| | | <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange" |
| | | :expand-row-keys="expandedRowKeys" :row-key="(row) => row.id" :row-class-name="tableRowClassName" show-summary style="width: 100%" |
| | |
| | | <template #default="props"> |
| | | <el-table :data="props.row.children" border show-summary :summary-method="summarizeChildrenTable"> |
| | | <el-table-column align="center" label="序号" type="index"/> |
| | | <el-table-column label="楼层编号" prop="floorCode" min-width="100" show-overflow-tooltip /> |
| | | <el-table-column label="产品大类" prop="productCategory" /> |
| | | <el-table-column label="规格型号" prop="specificationModel" /> |
| | | <el-table-column label="单位" prop="unit" /> |
| | | <el-table-column label="厚度" prop="thickness" min-width="90"> |
| | | <template #default="scope"> |
| | | {{ scope.row.thickness ?? "" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="宽(mm)" prop="width" min-width="80"> |
| | | <template #default="scope"> |
| | | {{ scope.row.width ?? "" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="高(mm)" prop="height" min-width="80"> |
| | | <template #default="scope"> |
| | | {{ scope.row.height ?? "" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="周长(cm)" prop="perimeter" min-width="90"> |
| | | <template #default="scope"> |
| | | {{ scope.row.perimeter ?? "" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="总面积(cm²)" prop="actualTotalArea" min-width="100"> |
| | | <template #default="scope"> |
| | | {{ scope.row.actualTotalArea ?? "" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="加工要求" prop="processRequirement" min-width="120" |
| | | show-overflow-tooltip /> |
| | | <el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip /> |
| | | <el-table-column label="重箱" prop="heavyBox" min-width="80"> |
| | | <template #default="scope"> |
| | | {{ scope.row.heavyBox ?? "" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="产品状态" |
| | | width="100px" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-tag v-if="scope.row.approveStatus === 1" |
| | | |
| | | <el-tag v-if="scope.row.approveStatus === 1 && (!scope.row.shippingDate || !scope.row.shippingCarNumber)" |
| | | type="success">充足</el-tag> |
| | | <el-tag v-else |
| | | type="danger">不足</el-tag> |
| | | <el-tag v-else-if="scope.row.approveStatus === 0 && (scope.row.shippingDate || scope.row.shippingCarNumber)" |
| | | type="success">已出库</el-tag> |
| | | <el-tag v-else type="danger">不足</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="发货状态" width="140" align="center"> |
| | |
| | | <el-table-column label="含税总价(元)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" /> |
| | | <el-table-column label="不含税总价(元)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" /> |
| | | <!--操作--> |
| | | <el-table-column Width="60px" label="操作" align="center"> |
| | | <!-- <el-table-column Width="60px" label="操作" align="center"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | size="small" |
| | | type="primary" |
| | | :disabled="!canShip(scope.row)" |
| | | @click="openDeliveryForm(scope.row)"> |
| | | 发货 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table-column> --> |
| | | </el-table> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | <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 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="操作" width="200" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="openForm('edit', 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>--> |
| | | <el-button link type="primary" @click="openForm('edit', scope.row)" :disabled="!scope.row.isEdit">编辑</el-button> |
| | | <el-button link type="primary" @click="openProcessFlowSelect(scope.row)" :disabled="!scope.row.isEdit">工艺路线</el-button> |
| | | <el-button link type="primary" @click="downLoadFile(scope.row)">附件</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | </el-row> |
| | | <el-table :data="productData" border @selection-change="productSelected" show-summary |
| | | :summary-method="summarizeMainTable"> |
| | | <el-table-column align="center" type="selection" width="55" v-if="operationType !== 'view'" /> |
| | | <el-table-column align="center" type="selection" width="55" v-if="operationType !== 'view'" |
| | | :selectable="(row) => !isProductShipped(row)" /> |
| | | <el-table-column align="center" label="序号" type="index" width="60" /> |
| | | <el-table-column label="产品大类" prop="productCategory" /> |
| | | <el-table-column label="规格型号" prop="specificationModel" /> |
| | | <el-table-column label="单位" prop="unit" /> |
| | | <el-table-column label="厚度" prop="thickness" min-width="90"> |
| | | <template #default="scope"> |
| | | {{ scope.row.thickness ?? "" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="宽(mm)" prop="width" min-width="80"> |
| | | <template #default="scope"> |
| | | {{ scope.row.width ?? "" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="高(mm)" prop="height" min-width="80"> |
| | | <template #default="scope"> |
| | | {{ scope.row.height ?? "" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="面积(m²)" prop="actualTotalArea" min-width="100"> |
| | | <template #default="scope"> |
| | | {{ scope.row.actualTotalArea ?? "" }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="数量" prop="quantity" /> |
| | | <el-table-column label="税率(%)" prop="taxRate" /> |
| | | <el-table-column label="含税单价(元)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" /> |
| | |
| | | <el-table-column label="不含税总价(元)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" /> |
| | | <el-table-column fixed="right" label="操作" min-width="60" align="center" v-if="operationType !== 'view'"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="openProductForm('edit', scope.row,scope.$index)">编辑</el-button> |
| | | <el-button link type="primary" size="small" |
| | | :disabled="isProductShipped(scope.row)" |
| | | @click="openProductForm('edit', scope.row,scope.$index)">编辑</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="备注·:" prop="remark"> |
| | | <el-input v-model="form.remark" placeholder="请输入" clearable type="textarea" :rows="2" :disabled="operationType === 'view'" /> |
| | | <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> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="附件材料:" prop="remark"> |
| | | <el-form-item label="客户备注:" prop="customerRemarks"> |
| | | <el-input |
| | | v-model="form.customerRemarks" |
| | | placeholder="请输入" |
| | | clearable |
| | | type="textarea" |
| | | :rows="2" |
| | | :disabled="operationType === 'view'" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="附件材料:" prop="salesLedgerFiles"> |
| | | <el-upload v-model:file-list="fileList" :action="upload.url" multiple ref="fileUpload" auto-upload |
| | | :headers="upload.headers" :before-upload="handleBeforeUpload" :on-error="handleUploadError" |
| | | :on-success="handleUploadSuccess" :on-remove="handleRemove"> |
| | |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <pagination |
| | | v-show="quotationPage.total > 0" |
| | | :total="quotationPage.total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :page="quotationPage.current" |
| | | :limit="quotationPage.size" |
| | | @pagination="quotationPaginationChange" |
| | | /> |
| | | |
| | | <template #footer> |
| | | <el-button @click="quotationDialogVisible = false">关闭</el-button> |
| | | </template> |
| | |
| | | <FormDialog |
| | | v-model="productFormVisible" |
| | | :title="productOperationType === 'add' ? '新增产品' : '编辑产品'" |
| | | :width="'40%'" |
| | | :width="'60%'" |
| | | :operation-type="productOperationType" |
| | | @close="closeProductDia" |
| | | @confirm="submitProduct" |
| | | @cancel="closeProductDia"> |
| | | <el-form :model="productForm" label-width="140px" label-position="top" :rules="productRules" ref="productFormRef"> |
| | | <!-- 每行三个:产品大类/规格型号/单位 --> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="产品大类:" prop="productCategory"> |
| | | <!-- <el-select v-model="productForm.productCategory" placeholder="请选择" clearable> |
| | | <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName"/> |
| | | </el-select> --> |
| | | <el-tree-select v-model="productForm.productCategory" placeholder="请选择" clearable check-strictly |
| | | @change="getModels" :data="productOptions" :render-after-expand="false" style="width: 100%" /> |
| | | <el-tree-select |
| | | v-model="productForm.productCategory" |
| | | placeholder="请选择" |
| | | clearable |
| | | check-strictly |
| | | @change="getModels" |
| | | :data="productOptions" |
| | | :render-after-expand="false" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="规格型号:" prop="productModelId"> |
| | | <el-select v-model="productForm.productModelId" placeholder="请选择" clearable @change="getProductModel" filterable> |
| | | <el-select |
| | | v-model="productForm.productModelId" |
| | | placeholder="请选择" |
| | | clearable |
| | | @change="getProductModel" |
| | | filterable |
| | | style="width: 100%" |
| | | > |
| | | <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="单位:" prop="unit"> |
| | | <el-input v-model="productForm.unit" placeholder="请输入" clearable /> |
| | | <el-col :span="8"> |
| | | <el-form-item label="厚度:" prop="thickness"> |
| | | <el-input-number |
| | | v-model="productForm.thickness" |
| | | :min="0" |
| | | :step="0.000000000000001" |
| | | :precision="15" |
| | | style="width: 100%;" |
| | | placeholder="请输入" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | </el-row> |
| | | |
| | | <!-- 每行三个:税率/含税单价/数量 --> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="税率(%):" prop="taxRate"> |
| | | <el-select v-model="productForm.taxRate" placeholder="请选择" clearable @change="calculateFromTaxRate"> |
| | | <el-select v-model="productForm.taxRate" placeholder="请选择" clearable @change="calculateFromTaxRate" style="width: 100%"> |
| | | <el-option label="1" value="1" /> |
| | | <el-option label="6" value="6" /> |
| | | <el-option label="13" value="13" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="含税单价(元):" prop="taxInclusiveUnitPrice"> |
| | | <el-input-number :step="0.01" :min="0" v-model="productForm.taxInclusiveUnitPrice" style="width: 100%" |
| | | :precision="2" |
| | | placeholder="请输入" clearable @change="calculateFromUnitPrice" /> |
| | | <el-input-number |
| | | :step="0.01" |
| | | :min="0" |
| | | v-model="productForm.taxInclusiveUnitPrice" |
| | | style="width: 100%" |
| | | :precision="2" |
| | | placeholder="请输入" |
| | | clearable |
| | | @change="calculateFromUnitPrice" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="数量:" prop="quantity"> |
| | | <el-input-number :step="0.1" :min="0" v-model="productForm.quantity" placeholder="请输入" clearable |
| | | :precision="2" |
| | | @change="calculateFromQuantity" style="width: 100%" /> |
| | | <el-input-number |
| | | :step="0.1" |
| | | :min="0" |
| | | v-model="productForm.quantity" |
| | | placeholder="请输入" |
| | | clearable |
| | | :precision="2" |
| | | @change="() => { calculateFromQuantity(); recalcAreaTotals(); }" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 每行三个:含税总价/不含税总价/发票类型 --> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="含税总价(元):" prop="taxInclusiveTotalPrice"> |
| | | <el-input v-model="productForm.taxInclusiveTotalPrice" placeholder="请输入" clearable @change="calculateFromTotalPrice" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="不含税总价(元):" prop="taxExclusiveTotalPrice"> |
| | | <el-input v-model="productForm.taxExclusiveTotalPrice" placeholder="请输入" clearable @change="calculateFromExclusiveTotalPrice" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="发票类型:" prop="invoiceType"> |
| | | <el-select v-model="productForm.invoiceType" placeholder="请选择" clearable> |
| | | <el-select v-model="productForm.invoiceType" placeholder="请选择" clearable style="width: 100%"> |
| | | <el-option label="增普票" value="增普票" /> |
| | | <el-option label="增专票" value="增专票" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 每行三个:宽/高/实际单片面积 --> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="宽(mm):" prop="width"> |
| | | <el-input-number |
| | | v-model="productForm.width" |
| | | :min="0" |
| | | :step="1" |
| | | :precision="2" |
| | | style="width: 100%" |
| | | placeholder="请输入宽(mm)" |
| | | clearable |
| | | @change="recalcAreaFromWidthHeight" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="高(mm):" prop="height"> |
| | | <el-input-number |
| | | v-model="productForm.height" |
| | | :min="0" |
| | | :step="1" |
| | | :precision="2" |
| | | style="width: 100%" |
| | | placeholder="请输入高(mm)" |
| | | clearable |
| | | @change="recalcAreaFromWidthHeight" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="周长(cm):" prop="perimeter"> |
| | | <el-input-number |
| | | v-model="productForm.perimeter" |
| | | :min="0" |
| | | :step="0.01" |
| | | :precision="2" |
| | | style="width: 100%" |
| | | placeholder="请输入" |
| | | clearable |
| | | :disabled="true" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 每行三个:实际单片面积/实际总面积/结算单片面积 --> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="实际单片面积(㎡):" prop="actualPieceArea"> |
| | | <el-input-number |
| | | v-model="productForm.actualPieceArea" |
| | | :min="0" |
| | | :step="0.00001" |
| | | :precision="5" |
| | | style="width: 100%" |
| | | placeholder="请输入" |
| | | clearable |
| | | @change="recalcAreaTotals" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="实际总面积(㎡):" prop="actualTotalArea"> |
| | | <el-input-number |
| | | v-model="productForm.actualTotalArea" |
| | | :min="0" |
| | | :step="0.00001" |
| | | :precision="5" |
| | | style="width: 100%" |
| | | placeholder="请输入" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="结算单片面积(㎡):" prop="settlePieceArea"> |
| | | <el-input-number |
| | | v-model="productForm.settlePieceArea" |
| | | :min="0" |
| | | :step="0.00001" |
| | | :precision="5" |
| | | style="width: 100%" |
| | | placeholder="请输入" |
| | | clearable |
| | | @change="() => { recalcAreaTotals(); calculateFromUnitPrice(true); }" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="结算总面积(㎡):" prop="settleTotalArea"> |
| | | <el-input-number |
| | | v-model="productForm.settleTotalArea" |
| | | :min="0" |
| | | :step="0.00001" |
| | | :precision="5" |
| | | style="width: 100%" |
| | | placeholder="请输入" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="结算总面积(㎡):" prop="settleTotalArea"> |
| | | <el-input-number |
| | | v-model="productForm.settleTotalArea" |
| | | :min="0" |
| | | :step="0.00001" |
| | | :precision="5" |
| | | style="width: 100%" |
| | | placeholder="请输入" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="重箱:" prop="heavyBox"> |
| | | <el-input v-model="productForm.heavyBox" placeholder="请输入" clearable @change="calculateFromExclusiveTotalPrice" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="楼层编号:" prop="floorCode"> |
| | | <el-input v-model="productForm.floorCode" placeholder="请输入楼层编号" clearable type="textarea" :rows="2" :disabled="operationType === 'view'" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <!-- 其他金额(占满一行:等同于 3 列网格的整行) --> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="加工要求:" prop="processRequirement"> |
| | | <el-input v-model="productForm.processRequirement" placeholder="请输入加工要求" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="备注:" prop="remark"> |
| | | <el-input v-model="productForm.remark" placeholder="请输入备注" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item> |
| | | <template #label> |
| | | <div style="display:flex; align-items:center; gap: 12px; width: 100%;"> |
| | | <div style="font-weight: 600; color: #303133;">其他金额:</div> |
| | | <div style="color:#909399; font-size: 13px; flex: 1;"> |
| | | 已选择 {{ productForm?.salesProductProcessList?.length || 0 }} 项 |
| | | </div> |
| | | <el-button |
| | | v-if="operationType !== 'view'" |
| | | type="primary" |
| | | plain |
| | | size="small" |
| | | @click="startAddOtherAmount" |
| | | > |
| | | 新增 |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | |
| | | <div style="display:flex; flex-direction:column; gap: 12px;"> |
| | | <div v-if="Array.isArray(productForm?.salesProductProcessList) && productForm.salesProductProcessList.length > 0" |
| | | style="display:flex; flex-wrap:wrap; gap: 12px; align-items:flex-start;" |
| | | > |
| | | <div |
| | | v-for="(item, index) in productForm.salesProductProcessList" |
| | | :key="String(item.id) + '_' + index" |
| | | style="display:flex; gap: 10px; align-items:center; padding: 10px 12px; border: 1px solid #ebeef5; border-radius: 8px; box-sizing:border-box; min-width: 0;" |
| | | :style="getOtherAmountCardFlexStyle()" |
| | | > |
| | | <div style="flex: 1; min-width: 0;"> |
| | | <el-tag type="info" style="width: 100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;"> |
| | | {{ item.processName }} |
| | | </el-tag> |
| | | </div> |
| | | <div style="flex: 1;"> |
| | | <el-input-number |
| | | v-model="item.quantity" |
| | | :min="0" |
| | | :step="0.1" |
| | | :precision="2" |
| | | style="width: 100%;" |
| | | placeholder="请输入数量" |
| | | :disabled="operationType === 'view'" |
| | | /> |
| | | </div> |
| | | <el-button |
| | | v-if="operationType !== 'view'" |
| | | type="danger" |
| | | link |
| | | size="small" |
| | | @click="removeOtherAmountAt(index)" |
| | | > |
| | | 删除 |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-else style="color:#909399; font-size: 13px;"> |
| | | 暂无其他金额 |
| | | </div> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | </FormDialog> |
| | | <!-- 其他金额:新增弹框 --> |
| | | <el-dialog |
| | | v-model="otherAmountAddDialogVisible" |
| | | title="新增其他金额" |
| | | width="520px" |
| | | :close-on-click-modal="false" |
| | | > |
| | | <div style="padding: 4px 0 10px;"> |
| | | <div style="font-size: 14px; color: #606266; margin-bottom: 10px;"> |
| | | 请选择要新增的其他金额项目 |
| | | </div> |
| | | <el-select |
| | | v-model="otherAmountAddId" |
| | | filterable |
| | | clearable |
| | | placeholder="请选择其他金额项目" |
| | | style="width: 100%;" |
| | | :disabled="operationType === 'view'" |
| | | > |
| | | <el-option |
| | | v-for="item in otherAmountSelectOptions" |
| | | :key="item.id" |
| | | :label="item.processName" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </div> |
| | | <template #footer> |
| | | <el-button @click="cancelAddOtherAmount">取消</el-button> |
| | | <el-button |
| | | type="primary" |
| | | @click="confirmAddOtherAmount" |
| | | :disabled="operationType === 'view' || otherAmountAddId === null || otherAmountAddId === undefined || otherAmountAddId === ''" |
| | | > |
| | | 确认添加 |
| | | </el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 导入弹窗 --> |
| | | <FormDialog |
| | | v-model="importUpload.open" |
| | |
| | | v-model="fileListDialogVisible" |
| | | title="附件列表" |
| | | /> |
| | | <!-- 打印预览弹窗 --> |
| | | <el-dialog |
| | | v-model="printPreviewVisible" |
| | | title="打印预览" |
| | | width="90%" |
| | | :close-on-click-modal="false" |
| | | class="print-preview-dialog" |
| | | > |
| | | <div class="print-preview-container"> |
| | | <div class="print-preview-header"> |
| | | <el-button type="primary" @click="executePrint">执行打印</el-button> |
| | | <el-button @click="printPreviewVisible = false">关闭预览</el-button> |
| | | </div> |
| | | <div class="print-preview-content"> |
| | | <div v-if="printData.length === 0" style="text-align: center; padding: 50px; color: #999;"> |
| | | 暂无打印数据 |
| | | </div> |
| | | <div v-else style="text-align: center; padding: 10px; color: #666; font-size: 14px; background: #e8f4fd; margin-bottom: 10px;"> |
| | | 共 {{ printData.length }} 条数据待打印 |
| | | </div> |
| | | <div v-for="(item, index) in printData" :key="index" class="print-page"> |
| | | <div class="delivery-note"> |
| | | <div class="header"> |
| | | <div class="company-name">鼎诚瑞实业有限责任公司</div> |
| | | <div class="document-title">零售发货单</div> |
| | | </div> |
| | | |
| | | <div class="info-section"> |
| | | <div class="info-row"> |
| | | <div> |
| | | <span class="label">发货日期:</span> |
| | | <span class="value">{{ formatDate(item.createTime) }}</span> |
| | | </div> |
| | | <div> |
| | | <span class="label">发货车牌号:</span> |
| | | <span class="value">{{ item.shippingCarNumber }}</span> |
| | | </div> |
| | | </div> |
| | | <div class="info-row"> |
| | | <div> |
| | | <span class="label">客户名称:</span> |
| | | <span class="value">{{ item.customerName || '张爱有' }}</span> |
| | | </div> |
| | | <span class="label">单号:</span> |
| | | <span class="value">{{ item.salesContractNo }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="table-section"> |
| | | <table class="product-table"> |
| | | <thead> |
| | | <tr> |
| | | <th>产品名称</th> |
| | | <th>规格型号</th> |
| | | <th>单位</th> |
| | | <th>单价</th> |
| | | <th>零售数量</th> |
| | | <th>零售金额</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <tr v-for="product in item.products" :key="product.id"> |
| | | <td>{{ product.productCategory || '' }}</td> |
| | | <td>{{ product.specificationModel || '' }}</td> |
| | | <td>{{ product.unit || '' }}</td> |
| | | <td>{{ product.taxInclusiveUnitPrice || '0' }}</td> |
| | | <td>{{ product.quantity || '0' }}</td> |
| | | <td>{{ product.taxInclusiveTotalPrice || '0' }}</td> |
| | | </tr> |
| | | <tr v-if="!item.products || item.products.length === 0"> |
| | | <td colspan="6" style="text-align: center; color: #999;">暂无产品数据</td> |
| | | </tr> |
| | | </tbody> |
| | | <tfoot> |
| | | <tr> |
| | | <td class="label">合计</td> |
| | | <td class="total-value"></td> |
| | | <td class="total-value"></td> |
| | | <td class="total-value"></td> |
| | | <td class="total-value">{{ getTotalQuantity(item.products) }}</td> |
| | | <td class="total-value">{{ getTotalAmount(item.products) }}</td> |
| | | </tr> |
| | | </tfoot> |
| | | </table> |
| | | </div> |
| | | |
| | | <div class="footer-section"> |
| | | <div class="footer-row"> |
| | | <div class="footer-item"> |
| | | <span class="label">收货电话:</span> |
| | | <span class="value"></span> |
| | | </div> |
| | | <div class="footer-item"> |
| | | <span class="label">收货人:</span> |
| | | <span class="value"></span> |
| | | </div> |
| | | <div class="footer-item address-item"> |
| | | <span class="label">收货地址:</span> |
| | | <span class="value address-value"></span> |
| | | </div> |
| | | </div> |
| | | <div class="footer-row"> |
| | | <div class="footer-item"> |
| | | <span class="label">操作员:</span> |
| | | <span class="value">{{ userStore.nickName || '撕开前' }}</span> |
| | | </div> |
| | | <div class="footer-item"> |
| | | <span class="label">打印日期:</span> |
| | | <span class="value">{{ formatDateTime(new Date()) }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-dialog> |
| | | <!-- 发货弹框 --> |
| | | <el-dialog |
| | | v-model="deliveryFormVisible" |
| | |
| | | <div> |
| | | <el-button |
| | | type="danger" |
| | | size="small" |
| | | @click="removeApproverNode(index)" |
| | | v-if="approverNodes.length > 1" |
| | | >删除</el-button> |
| | |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | import {onMounted, ref, getCurrentInstance} from "vue"; |
| | | import { addShippingInfo } from "@/api/salesManagement/deliveryLedger.js"; |
| | | import { ElMessageBox, ElMessage } from "element-plus"; |
| | | import { UploadFilled, Download } from "@element-plus/icons-vue"; |
| | | import { ArrowDown } from "@element-plus/icons-vue"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { userListNoPage } from "@/api/system/user.js"; |
| | | import FileListDialog from '@/components/Dialog/FileListDialog.vue'; |
| | | import FormDialog from '@/components/Dialog/FormDialog.vue'; |
| | | import OtherAmountMaintenanceButton from "./components/OtherAmountMaintenanceButton.vue"; |
| | | import ProcessFlowMaintenanceButton from "./components/ProcessFlowMaintenanceButton.vue"; |
| | | import ProcessFlowConfigSelectDialog from "./components/ProcessFlowConfigSelectDialog.vue"; |
| | | import { getQuotationList } from "@/api/salesManagement/salesQuotation.js"; |
| | | import { |
| | | ledgerListPage, |
| | |
| | | delLedger, |
| | | addOrUpdateSalesLedgerProduct, |
| | | delProduct, |
| | | delLedgerFile, getProductInventory, |
| | | delLedgerFile, |
| | | getProductInventory, |
| | | salesLedgerProductProcessList, |
| | | saleProcessBind, |
| | | getSaleProcessBindInfo, |
| | | getProcessCard, |
| | | getSalesOrder, |
| | | getSalesInvoices, |
| | | getSalesLabel, |
| | | } from "@/api/salesManagement/salesLedger.js"; |
| | | import { modelList, productTreeList } from "@/api/basicData/product.js"; |
| | | import useFormData from "@/hooks/useFormData.js"; |
| | | import dayjs from "dayjs"; |
| | | import { getCurrentDate } from "@/utils/index.js"; |
| | | import { printFinishedProcessCard } from "./components/processCardPrint.js"; |
| | | import { printSalesOrder } from "./components/salesOrderPrint.js"; |
| | | import { printSalesDeliveryNote } from "./components/salesDeliveryPrint.js"; |
| | | import { printSalesLabel } from "./components/salesLabelPrint.js"; |
| | | // import { salesLedgerProductSetProcessFlowConfig } from "@/api/salesManagement/salesProcessFlowConfig.js"; |
| | | |
| | | const userStore = useUserStore(); |
| | | const { proxy } = getCurrentInstance(); |
| | |
| | | }); |
| | | const total = ref(0); |
| | | const fileList = ref([]); |
| | | |
| | | // 工艺路线配置选择弹窗(绑定到台账产品) |
| | | const processFlowSelectDialogVisible = ref(false); |
| | | const processFlowSelectLedgerRow = ref(null); |
| | | const processFlowSelectDefaultRouteId = ref(null); |
| | | const processFlowSelectBoundRouteId = ref(null); |
| | | const processFlowSelectBoundRouteName = ref(""); |
| | | |
| | | // 用户信息表单弹框数据 |
| | | const operationType = ref(""); |
| | |
| | | productForm: { |
| | | productCategory: "", |
| | | specificationModel: "", |
| | | unit: "", |
| | | thickness:null, |
| | | quantity: "", |
| | | taxInclusiveUnitPrice: "", |
| | | taxRate: "", |
| | | taxInclusiveTotalPrice: "", |
| | | taxExclusiveTotalPrice: "", |
| | | invoiceType: "", |
| | | // 新增:尺寸/面积/加工与其他金额 |
| | | width: 0, // 宽(mm) |
| | | height: 0, // 高(mm) |
| | | perimeter: 0, // 周长(cm) = (宽+高)*2,宽高为 mm |
| | | // 面积字段(㎡) |
| | | actualPieceArea: 0, // 实际单片面积(㎡) |
| | | actualTotalArea: 0, // 实际总面积(㎡) |
| | | settlePieceArea: 0, // 结算单片面积(㎡) |
| | | settleTotalArea: 0, // 结算总面积(㎡) |
| | | processRequirement: "", // 加工要求 |
| | | remark: "", // 备注 |
| | | salesProductProcessList: [], // 其他金额:[{id, processName, quantity}] |
| | | processFlowConfigId: null, // 工艺流程配置绑定 |
| | | floorCode: "", // 楼层编号 |
| | | }, |
| | | productRules: { |
| | | productCategory: [{ required: true, message: "请选择", trigger: "change" }], |
| | |
| | | specificationModel: [ |
| | | { required: true, message: "请选择", trigger: "change" }, |
| | | ], |
| | | unit: [{ required: true, message: "请输入", trigger: "blur" }], |
| | | thickness: [{ required: true, message: "请输入", trigger: "blur" }], |
| | | quantity: [{ required: true, message: "请输入", trigger: "blur" }], |
| | | taxInclusiveUnitPrice: [ |
| | | { required: true, message: "请输入", trigger: "blur" }, |
| | |
| | | // 设置上传的请求头部 |
| | | headers: { Authorization: "Bearer " + getToken() }, |
| | | }); |
| | | // 打印相关 |
| | | const printPreviewVisible = ref(false); |
| | | const printData = ref([]); |
| | | |
| | | // 报价单导入相关 |
| | | const quotationDialogVisible = ref(false); |
| | | const quotationLoading = ref(false); |
| | |
| | | quotationNo: "", |
| | | customer: "", |
| | | }); |
| | | // 报价单弹框分页 |
| | | const quotationPage = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | const selectedQuotation = ref(null); |
| | | |
| | | // 发货相关 |
| | | const deliveryFormVisible = ref(false); |
| | | const currentDeliveryRow = ref(null); |
| | | const currentDeliveryRows = ref([]); |
| | | const deliveryFormData = reactive({ |
| | | deliveryForm: { |
| | | type: "货车", // 货车, 快递 |
| | |
| | | }, |
| | | }); |
| | | const { deliveryForm, deliveryRules } = toRefs(deliveryFormData); |
| | | |
| | | // 产品弹框:其他金额多选下拉(基于“其他金额维护”查询接口) |
| | | const otherAmountSelectOptions = ref([]); // [{id, processName}] |
| | | const otherAmountSelectOptionsLoading = ref(false); |
| | | |
| | | const fetchOtherAmountSelectOptions = async (force = false) => { |
| | | if (!force && otherAmountSelectOptions.value.length > 0) return; |
| | | otherAmountSelectOptionsLoading.value = true; |
| | | try { |
| | | const params = { |
| | | current: 1, |
| | | // 下拉框尽量一次性拉全,避免多次分页影响选择体验 |
| | | size: 1000, |
| | | }; |
| | | const res = await salesLedgerProductProcessList(params); |
| | | const records = res?.records ?? res?.data?.records ?? []; |
| | | |
| | | otherAmountSelectOptions.value = records.map((item) => ({ |
| | | id: item.id, |
| | | processName: item.processName ?? "", |
| | | })); |
| | | } finally { |
| | | otherAmountSelectOptionsLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const normalizeOtherAmountsFromRow = (row) => { |
| | | if (!row) return []; |
| | | const raw = |
| | | row.other_amounts ?? |
| | | row.otherAmounts ?? |
| | | row.otherAmountProjects ?? |
| | | row.otherAmountList ?? |
| | | row.salesProductProcessList ?? |
| | | []; |
| | | |
| | | if (!Array.isArray(raw)) return []; |
| | | if (raw.length === 0) return []; |
| | | |
| | | // 情况1:后端直接返回 [{id, processName}...] |
| | | if (typeof raw[0] === "object") { |
| | | return raw |
| | | .map((it) => { |
| | | const id = it?.id ?? it?.processId ?? it?.otherAmountId ?? null; |
| | | const quantity = |
| | | Number( |
| | | it?.quantity ?? |
| | | it?.processQuantity ?? |
| | | it?.otherQuantity ?? |
| | | it?.process_quantity ?? |
| | | it?.other_amount_quantity |
| | | ) || 0; |
| | | return { |
| | | id, |
| | | processName: it?.processName ?? "", |
| | | quantity, |
| | | }; |
| | | }) |
| | | .filter((it) => it.id !== null && it.id !== undefined && it.id !== ""); |
| | | } |
| | | |
| | | // 情况2:后端只返回 ids: [1,2,3] |
| | | return raw |
| | | .map((id) => ({ |
| | | id, |
| | | processName: "", |
| | | quantity: 0, |
| | | })) |
| | | .filter((it) => it.id !== null && it.id !== undefined && it.id !== ""); |
| | | }; |
| | | |
| | | const mergeOtherAmountOptionsBySelection = (selected) => { |
| | | const list = Array.isArray(selected) ? selected : []; |
| | | for (const s of list) { |
| | | const exists = otherAmountSelectOptions.value.some((o) => String(o.id) === String(s.id)); |
| | | if (!exists) { |
| | | otherAmountSelectOptions.value.push({ |
| | | id: s.id, |
| | | processName: s.processName ?? "", |
| | | }); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const fillOtherAmountProcessName = (selected) => { |
| | | const list = Array.isArray(selected) ? selected : []; |
| | | return list.map((s) => { |
| | | const opt = otherAmountSelectOptions.value.find((o) => String(o.id) === String(s.id)); |
| | | return { |
| | | id: s.id, |
| | | processName: opt?.processName ?? s.processName ?? "", |
| | | quantity: Number(s.quantity ?? 0) || 0, |
| | | }; |
| | | }); |
| | | }; |
| | | |
| | | // “其他金额”卡片布局:只有一条时占满整行;>=2时两列排布 |
| | | const getOtherAmountCardFlexStyle = () => { |
| | | const list = productForm.value?.salesProductProcessList; |
| | | const len = Array.isArray(list) ? list.length : 0; |
| | | if (len === 1) { |
| | | return { flex: "0 0 100%", maxWidth: "100%", width: "100%" }; |
| | | } |
| | | return { |
| | | flex: "0 0 calc(50% - 6px)", |
| | | maxWidth: "calc(50% - 6px)", |
| | | width: "calc(50% - 6px)", |
| | | }; |
| | | }; |
| | | |
| | | // 其他金额:点击“新增”后在弹窗里选择一个项目 |
| | | const otherAmountAddDialogVisible = ref(false); |
| | | const otherAmountAddId = ref(null); |
| | | |
| | | const startAddOtherAmount = () => { |
| | | if (operationType.value === "view") return; |
| | | otherAmountAddDialogVisible.value = true; |
| | | otherAmountAddId.value = null; |
| | | // 通常 openProductForm 已经拉过 options,这里兜底 |
| | | if (otherAmountSelectOptions.value.length === 0) { |
| | | fetchOtherAmountSelectOptions(true); |
| | | } |
| | | }; |
| | | |
| | | const cancelAddOtherAmount = () => { |
| | | otherAmountAddDialogVisible.value = false; |
| | | otherAmountAddId.value = null; |
| | | }; |
| | | |
| | | const handleOtherAmountSelected = (id) => { |
| | | const selectedId = id ?? otherAmountAddId.value; |
| | | if (selectedId === null || selectedId === undefined || selectedId === "") return; |
| | | const opt = otherAmountSelectOptions.value.find((o) => String(o.id) === String(selectedId)); |
| | | if (!opt) return; |
| | | |
| | | const exists = (productForm.value?.salesProductProcessList ?? []).some( |
| | | (it) => String(it?.id) === String(opt.id) |
| | | ); |
| | | if (exists) { |
| | | proxy.$modal.msgWarning("该其他金额项目已添加"); |
| | | return; |
| | | } |
| | | |
| | | productForm.value.salesProductProcessList.push({ |
| | | id: opt.id, |
| | | processName: opt.processName, |
| | | quantity: 0, |
| | | }); |
| | | |
| | | // 选择完成后关闭弹窗,下一次可再次点击“新增”继续添加 |
| | | otherAmountAddDialogVisible.value = false; |
| | | otherAmountAddId.value = null; |
| | | }; |
| | | |
| | | const confirmAddOtherAmount = () => { |
| | | handleOtherAmountSelected(otherAmountAddId.value); |
| | | }; |
| | | |
| | | const removeOtherAmountAt = (index) => { |
| | | if (operationType.value === "view") return; |
| | | if (!Array.isArray(productForm.value?.salesProductProcessList)) return; |
| | | productForm.value.salesProductProcessList.splice(index, 1); |
| | | }; |
| | | |
| | | // 发货审批人节点(仿协同审批 infoFormDia.vue) |
| | | const approverNodes = ref([{ id: 1, userId: null }]); |
| | |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | // 打开“工艺路线配置”选择弹窗(必须显式选择) |
| | | const openProcessFlowSelect = async (ledgerRow) => { |
| | | if (!ledgerRow) return; |
| | | if (!ledgerRow.isEdit) return; |
| | | |
| | | processFlowSelectLedgerRow.value = ledgerRow; |
| | | processFlowSelectDefaultRouteId.value = null; |
| | | processFlowSelectBoundRouteId.value = null; |
| | | processFlowSelectBoundRouteName.value = ""; |
| | | |
| | | try { |
| | | const res = await getSaleProcessBindInfo(ledgerRow.id); |
| | | const info = res?.data ?? res ?? {}; |
| | | const boundId = |
| | | info?.processRouteId ?? |
| | | info?.routeId ?? |
| | | info?.id ?? |
| | | null; |
| | | const boundName = |
| | | info?.processRouteName ?? |
| | | info?.routeName ?? |
| | | info?.name ?? |
| | | ""; |
| | | processFlowSelectBoundRouteId.value = boundId; |
| | | processFlowSelectBoundRouteName.value = boundName; |
| | | processFlowSelectDefaultRouteId.value = boundId; |
| | | } catch (e) { |
| | | // 查询失败时按未绑定处理,不阻塞弹窗 |
| | | processFlowSelectBoundRouteId.value = null; |
| | | processFlowSelectBoundRouteName.value = ""; |
| | | processFlowSelectDefaultRouteId.value = null; |
| | | } |
| | | |
| | | processFlowSelectDialogVisible.value = true; |
| | | }; |
| | | |
| | | // 绑定工艺路线到当前台账数据 |
| | | const handleProcessFlowSelectConfirm = async (routeId) => { |
| | | const ledgerRow = processFlowSelectLedgerRow.value; |
| | | if (!ledgerRow?.id) return; |
| | | |
| | | const finalRouteId = routeId ?? null; |
| | | if (!finalRouteId) return; |
| | | |
| | | const oldRouteId = processFlowSelectBoundRouteId.value; |
| | | if (oldRouteId !== null && oldRouteId !== undefined && oldRouteId !== "" && String(oldRouteId) !== String(finalRouteId)) { |
| | | try { |
| | | await ElMessageBox.confirm( |
| | | "该订单已绑定工艺路线,是否确定更换?", |
| | | "提示", |
| | | { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | } |
| | | ); |
| | | } catch { |
| | | return; |
| | | } |
| | | } |
| | | |
| | | proxy?.$modal?.loading?.("正在绑定工艺路线,请稍候..."); |
| | | try { |
| | | await saleProcessBind({ |
| | | salesLedgerId: ledgerRow.id, |
| | | processRouteId: finalRouteId, |
| | | }); |
| | | |
| | | proxy?.$modal?.msgSuccess?.("工艺路线绑定成功"); |
| | | processFlowSelectDialogVisible.value = false; |
| | | // 绑定后刷新列表,确保操作列再次点击能回显绑定 |
| | | await getList(); |
| | | } catch (e) { |
| | | proxy?.$modal?.msgError?.("绑定失败,请稍后重试"); |
| | | } finally { |
| | | proxy?.$modal?.closeLoading?.(); |
| | | } |
| | | }; |
| | | |
| | | // 获取产品大类tree数据 |
| | | const getProductOptions = () => { |
| | | // 返回 Promise,便于在编辑产品时等待加载完成 |
| | |
| | | const index = modelOptions.value.findIndex((item) => item.id === value); |
| | | if (index !== -1) { |
| | | productForm.value.specificationModel = modelOptions.value[index].model; |
| | | productForm.value.unit = modelOptions.value[index].unit; |
| | | } else { |
| | | productForm.value.specificationModel = null; |
| | | productForm.value.unit = null; |
| | | } |
| | | }; |
| | | const findNodeById = (nodes, productId) => { |
| | |
| | | |
| | | // 添加表行类名方法 |
| | | const tableRowClassName = ({ row }) => { |
| | | const diff = row.deliveryDaysDiff; |
| | | if (!row.deliveryDate) return ''; |
| | | if (row.isFh) return ''; |
| | | |
| | | const diff = row.deliveryDaysDiff; |
| | | if (diff === 15) { |
| | | return 'yellow'; |
| | | } else if (diff === 10) { |
| | |
| | | form.value.entryDate = getCurrentDate(); |
| | | // 签订日期默认为当天 |
| | | form.value.executionDate = getCurrentDate(); |
| | | form.value.customerRemarks = ""; |
| | | } else { |
| | | currentId.value = row.id; |
| | | getSalesLedgerWithProducts({ id: row.id, type: 1 }).then((res) => { |
| | | form.value = { ...res }; |
| | | form.value.entryPerson = Number(res.entryPerson); |
| | | // 字段名兼容:后端可能返回 customer_remarks |
| | | form.value.customerRemarks = res?.customerRemarks ?? res?.customer_remarks ?? ""; |
| | | productData.value = form.value.productData; |
| | | fileList.value = form.value.salesLedgerFiles; |
| | | }); |
| | |
| | | const openQuotationDialog = async () => { |
| | | if (operationType.value === "view") return; |
| | | quotationDialogVisible.value = true; |
| | | // 打开弹窗时重置分页到第一页 |
| | | quotationPage.current = 1; |
| | | // 先确保客户列表已加载,便于后续回填 customerId |
| | | if (!customerOption.value || customerOption.value.length === 0) { |
| | | try { |
| | |
| | | quotationLoading.value = true; |
| | | try { |
| | | const params = { |
| | | // 兼容后端分页字段:这里沿用报价页面已有可用的字段命名 |
| | | currentPage: 1, |
| | | pageSize: 100, |
| | | // 后端分页字段:current / size |
| | | current: quotationPage.current, |
| | | size: quotationPage.size, |
| | | ...quotationSearchForm, |
| | | status: "通过", |
| | | }; |
| | | const res = await getQuotationList(params); |
| | | quotationList.value = res?.data?.records || []; |
| | | quotationPage.total = res?.data?.total || 0; |
| | | } finally { |
| | | quotationLoading.value = false; |
| | | } |
| | |
| | | const resetQuotationSearch = async () => { |
| | | quotationSearchForm.quotationNo = ""; |
| | | quotationSearchForm.customer = ""; |
| | | quotationPage.current = 1; |
| | | await fetchQuotationList(); |
| | | }; |
| | | |
| | | // 报价单弹框分页切换 |
| | | const quotationPaginationChange = (obj) => { |
| | | quotationPage.current = obj.page; |
| | | quotationPage.size = obj.limit; |
| | | fetchQuotationList(); |
| | | }; |
| | | |
| | | // 选中报价单后回填到台账表单 |
| | |
| | | productData.value = products.map((p) => { |
| | | const quantity = Number(p.quantity ?? 0) || 0; |
| | | const unitPrice = Number(p.unitPrice ?? 0) || 0; |
| | | const settlePieceArea = Number(p.settlePieceArea ?? 0) || 1; |
| | | const taxRate = "13"; // 默认 13%,便于直接提交(如需可在产品中自行修改) |
| | | const taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2); |
| | | const taxInclusiveTotalPrice = (unitPrice * settlePieceArea * quantity).toFixed(2); |
| | | const taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(taxInclusiveTotalPrice, taxRate); |
| | | return { |
| | | // 台账字段 |
| | | productCategory: p.product || p.productName || "", |
| | | specificationModel: p.specification || "", |
| | | unit: p.unit || "", |
| | | thickness: p.thickness, |
| | | quantity: quantity, |
| | | taxRate: taxRate, |
| | | taxInclusiveUnitPrice: unitPrice.toFixed(2), |
| | | taxInclusiveTotalPrice: taxInclusiveTotalPrice, |
| | | taxExclusiveTotalPrice: taxExclusiveTotalPrice, |
| | | invoiceType: "增普票", |
| | | // 新增:默认值(避免后续提交时字段缺失) |
| | | width: 0, |
| | | height: 0, |
| | | actualPieceArea: 0, |
| | | actualTotalArea: 0, |
| | | settlePieceArea: 0, |
| | | settleTotalArea: 0, |
| | | processRequirement: "", |
| | | floorCode: "", |
| | | remark: "", |
| | | salesProductProcessList: [], |
| | | }; |
| | | }); |
| | | |
| | |
| | | const productIndex = ref(0); |
| | | // 打开产品弹框 |
| | | const openProductForm = async (type, row, index) => { |
| | | // 编辑时检查产品是否已发货或审核通过 |
| | | if (type === "edit" && isProductShipped(row)) { |
| | | proxy.$modal.msgWarning("已发货或审核通过的产品不能编辑"); |
| | | return; |
| | | } |
| | | |
| | | productOperationType.value = type; |
| | | productForm.value = {}; |
| | | proxy.resetForm("productFormRef"); |
| | | // 确保多选项默认是数组,避免 el-select multiple 报错 |
| | | productForm.value.salesProductProcessList = []; |
| | | otherAmountAddDialogVisible.value = false; |
| | | otherAmountAddId.value = null; |
| | | if (type === "edit") { |
| | | productForm.value = { ...row }; |
| | | |
| | | // 字段命名兼容:优先驼峰(如 actualPieceArea),兼容后端可能返回下划线(如 actual_piece_area) |
| | | productForm.value.actualPieceArea = row?.actualPieceArea ?? row?.actual_piece_area ?? 0; |
| | | productForm.value.actualTotalArea = row?.actualTotalArea ?? row?.actual_total_area ?? 0; |
| | | productForm.value.settlePieceArea = row?.settlePieceArea ?? row?.settle_piece_area ?? 0; |
| | | productForm.value.settleTotalArea = row?.settleTotalArea ?? row?.settle_total_area ?? 0; |
| | | |
| | | // 加工要求/备注兼容:后端可能使用其它命名 |
| | | productForm.value.processRequirement = |
| | | row?.processRequirement ?? row?.process_requirement ?? ""; |
| | | productForm.value.remark = row?.remark ?? row?.remarks ?? ""; |
| | | productForm.value.floorCode = row?.floorCode ?? row?.floor_code ?? ""; |
| | | // 工艺流程配置绑定字段(后续由后端确认字段名) |
| | | productForm.value.processFlowConfigId = |
| | | row?.processFlowConfigId ?? row?.process_flow_config_id ?? null; |
| | | |
| | | // 周长回显(如后端返回;最终仍以公式计算为准) |
| | | productForm.value.perimeter = |
| | | row?.perimeter ?? row?.heavyBoxPerimeter ?? row?.heavyboxPerimeter ?? 0; |
| | | |
| | | // 后端直接返回 thickness |
| | | productForm.value.thickness = row?.thickness; |
| | | |
| | | productForm.value.salesProductProcessList = normalizeOtherAmountsFromRow(row); |
| | | productIndex.value = index; |
| | | // 编辑时根据产品大类名称反查 tree 节点 id,并加载规格型号列表 |
| | | try { |
| | |
| | | // 加载失败时保持可编辑,不中断弹窗 |
| | | console.error("加载产品规格型号失败", e); |
| | | } |
| | | |
| | | // 根据当前宽高重新计算周长与面积 |
| | | recalcPerimeterFromWidthHeight(); |
| | | recalcAreaFromWidthHeight(); |
| | | |
| | | // 回显“其他金额”多选:先拉取下拉选项,再补齐 processName |
| | | await fetchOtherAmountSelectOptions(true); |
| | | mergeOtherAmountOptionsBySelection(productForm.value.salesProductProcessList); |
| | | productForm.value.salesProductProcessList = fillOtherAmountProcessName(productForm.value.salesProductProcessList); |
| | | } else { |
| | | getProductOptions() |
| | | // 新增时下拉选项加载一次即可 |
| | | fetchOtherAmountSelectOptions(true); |
| | | } |
| | | productFormVisible.value = true; |
| | | }; |
| | |
| | | const submitProduct = () => { |
| | | proxy.$refs["productFormRef"].validate((valid) => { |
| | | if (valid) { |
| | | // 厚度保留 15 位小数,避免由于浮点计算/输入导致精度偏差 |
| | | if (productForm.value.thickness !== null && productForm.value.thickness !== undefined) { |
| | | productForm.value.thickness = Number(Number(productForm.value.thickness).toFixed(15)); |
| | | } |
| | | |
| | | // 面积/总计字段在提交前兜底计算一次 |
| | | recalcAreaTotals(); |
| | | // 其他金额只提交 {id, processName, quantity}(后端字段:salesProductProcessList) |
| | | productForm.value.salesProductProcessList = (Array.isArray(productForm.value.salesProductProcessList) |
| | | ? productForm.value.salesProductProcessList |
| | | : [] |
| | | ) |
| | | .map((it) => ({ |
| | | id: it?.id, |
| | | processName: it?.processName ?? "", |
| | | quantity: Number(it?.quantity ?? 0) || 0, |
| | | })) |
| | | .filter((it) => it.id !== null && it.id !== undefined && it.id !== ""); |
| | | |
| | | if (operationType.value === "edit") { |
| | | submitProductEdit(); |
| | | } else { |
| | |
| | | proxy.$modal.msgWarning("请选择数据"); |
| | | return; |
| | | } |
| | | |
| | | // 检查是否有已发货或审核通过的产品 |
| | | const shippedProducts = productSelectedRows.value.filter(row => isProductShipped(row)); |
| | | if (shippedProducts.length > 0) { |
| | | proxy.$modal.msgWarning("已发货或审核通过的产品不能删除"); |
| | | return; |
| | | } |
| | | |
| | | if (operationType.value === "add") { |
| | | productSelectedRows.value.forEach((selectedRow) => { |
| | | const index = productData.value.findIndex( |
| | |
| | | const closeProductDia = () => { |
| | | proxy.resetForm("productFormRef"); |
| | | productFormVisible.value = false; |
| | | otherAmountAddDialogVisible.value = false; |
| | | otherAmountAddId.value = null; |
| | | }; |
| | | // 导入 |
| | | const handleImport = () => { |
| | |
| | | proxy.$modal.msg("已取消"); |
| | | }); |
| | | }; |
| | | /** 判断单个产品是否已发货(根据shippingStatus判断,已发货或审核通过不可编辑和删除) */ |
| | | const isProductShipped = (product) => { |
| | | if (!product) return false; |
| | | const status = String(product.shippingStatus || "").trim(); |
| | | // 如果发货状态是"已发货"或"审核通过",则不可编辑和删除 |
| | | return status === "已发货" || status === "审核通过"; |
| | | }; |
| | | |
| | | /** 判断销售订单下是否存在已发货/发货完成的产品(不可删除) */ |
| | | const hasShippedProducts = (products) => { |
| | | if (!products || !products.length) return false; |
| | | return products.some((p) => { |
| | | const status = String(p.shippingStatus || "").trim(); |
| | | // 有发货日期或车牌号视为已发货 |
| | | if (p.shippingDate || p.shippingCarNumber) return true; |
| | | // 已进行发货、发货完成、已发货 均不可删除 |
| | | return status === "已进行发货" || status === "发货完成" || status === "已发货"; |
| | | }); |
| | | }; |
| | | |
| | | // 删除 |
| | | const handleDelete = () => { |
| | | let ids = []; |
| | | if (selectedRows.value.length > 0) { |
| | | ids = selectedRows.value.map((item) => item.id); |
| | | } else { |
| | | const handleDelete = async () => { |
| | | if (selectedRows.value.length === 0) { |
| | | proxy.$modal.msgWarning("请选择数据"); |
| | | return; |
| | | } |
| | | const ids = selectedRows.value.map((item) => item.id); |
| | | |
| | | // 检查是否有已进行发货或发货完成的销售订单,若有则不允许删除 |
| | | const cannotDeleteNames = []; |
| | | for (const row of selectedRows.value) { |
| | | let products = row.children && row.children.length > 0 ? row.children : null; |
| | | if (!products) { |
| | | try { |
| | | const res = await productList({ salesLedgerId: row.id, type: 1 }); |
| | | products = res.data || []; |
| | | } catch { |
| | | products = []; |
| | | } |
| | | } |
| | | if (hasShippedProducts(products)) { |
| | | cannotDeleteNames.push(row.salesContractNo || `ID:${row.id}`); |
| | | } |
| | | } |
| | | if (cannotDeleteNames.length > 0) { |
| | | proxy.$modal.msgWarning("已进行发货或发货完成的销售订单不能删除:" + cannotDeleteNames.join("、")); |
| | | return; |
| | | } |
| | | |
| | | ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "取消", |
| | |
| | | }); |
| | | }; |
| | | |
| | | // 打印功能 |
| | | const handlePrint = async () => { |
| | | if (selectedRows.value.length === 0) { |
| | | proxy.$modal.msgWarning("请选择要打印的数据"); |
| | | const handlePrintCommand = async (command) => { |
| | | if (command !== "finishedProcessCard" && command !== "salesOrder" && command !== "salesDeliveryNote") return; |
| | | if (command === "salesDeliveryNote") { |
| | | if (selectedRows.value.length === 0) { |
| | | proxy.$modal.msgWarning("请至少选择一条销售台账数据进行打印"); |
| | | return; |
| | | } |
| | | const customerNames = Array.from( |
| | | new Set(selectedRows.value.map((item) => String(item?.customerName ?? "").trim())) |
| | | ); |
| | | if (customerNames.length > 1) { |
| | | proxy.$modal.msgWarning("仅支持相同客户名称的销售台账合并发货打印"); |
| | | return; |
| | | } |
| | | } else if (selectedRows.value.length !== 1) { |
| | | proxy.$modal.msgWarning("请选择一条销售台账数据进行打印"); |
| | | return; |
| | | } |
| | | |
| | | // 显示加载状态 |
| | | proxy.$modal.loading("正在获取产品数据,请稍候..."); |
| | | |
| | | try { |
| | | // 为每个选中的销售台账记录查询对应的产品数据 |
| | | const printDataWithProducts = []; |
| | | |
| | | for (const row of selectedRows.value) { |
| | | try { |
| | | // 调用productList接口查询产品数据 |
| | | const productRes = await productList({ salesLedgerId: row.id, type: 1 }); |
| | | |
| | | // 将产品数据整合到销售台账记录中 |
| | | const rowWithProducts = { |
| | | ...row, |
| | | products: productRes.data || [] |
| | | }; |
| | | |
| | | printDataWithProducts.push(rowWithProducts); |
| | | } catch (error) { |
| | | console.error(`获取销售台账 ${row.id} 的产品数据失败:`, error); |
| | | // 即使某个记录的产品数据获取失败,也要包含该记录 |
| | | printDataWithProducts.push({ |
| | | ...row, |
| | | products: [] |
| | | }); |
| | | } |
| | | |
| | | const selectedRow = selectedRows.value[0]; |
| | | const selectedId = selectedRow?.id; |
| | | if (command === "salesDeliveryNote") { |
| | | const selectedIds = selectedRows.value |
| | | .map((item) => item?.id) |
| | | .filter((id) => id !== null && id !== undefined && id !== ""); |
| | | if (selectedIds.length !== selectedRows.value.length) { |
| | | proxy.$modal.msgWarning("当前选择数据存在缺少ID的记录,无法打印"); |
| | | return; |
| | | } |
| | | |
| | | printData.value = printDataWithProducts; |
| | | console.log('打印数据(包含产品):', printData.value); |
| | | printPreviewVisible.value = true; |
| | | |
| | | const loadingText = |
| | | command === "salesOrder" |
| | | ? "正在获取销售订单数据,请稍候..." |
| | | : command === "salesDeliveryNote" |
| | | ? "正在获取销售发货单数据,请稍候..." |
| | | : "正在获取生产流程卡数据,请稍候..."; |
| | | proxy.$modal.loading(loadingText); |
| | | try { |
| | | const res = await getSalesInvoices(selectedIds); |
| | | const salesInvoiceData = res?.data ?? {}; |
| | | printSalesDeliveryNote(salesInvoiceData, selectedRow); |
| | | } catch (error) { |
| | | console.error("打印销售发货单失败:", error); |
| | | proxy.$modal.msgError("打印失败,请稍后重试"); |
| | | } finally { |
| | | proxy.$modal.closeLoading(); |
| | | } |
| | | return; |
| | | } |
| | | if (!selectedId) { |
| | | proxy.$modal.msgWarning("当前选择数据缺少ID,无法打印"); |
| | | return; |
| | | } |
| | | |
| | | const loadingText = |
| | | command === "salesOrder" |
| | | ? "正在获取销售订单数据,请稍候..." |
| | | : command === "salesDeliveryNote" |
| | | ? "正在获取销售发货单数据,请稍候..." |
| | | : "正在获取生产流程卡数据,请稍候..."; |
| | | proxy.$modal.loading(loadingText); |
| | | try { |
| | | if (command === "salesOrder") { |
| | | const res = await getSalesOrder(selectedId); |
| | | const salesOrderData = res?.data ?? {}; |
| | | printSalesOrder(salesOrderData); |
| | | } else { |
| | | const res = await getProcessCard(selectedId); |
| | | const processCardData = res?.data ?? {}; |
| | | printFinishedProcessCard(processCardData); |
| | | } |
| | | } catch (error) { |
| | | console.error('获取产品数据失败:', error); |
| | | proxy.$modal.msgError("获取产品数据失败,请重试"); |
| | | console.error( |
| | | command === "salesOrder" |
| | | ? "打印销售订单失败:" |
| | | : command === "salesDeliveryNote" |
| | | ? "打印销售发货单失败:" |
| | | : "打印生产流程卡失败:", |
| | | error |
| | | ); |
| | | proxy.$modal.msgError("打印失败,请稍后重试"); |
| | | } finally { |
| | | proxy.$modal.closeLoading(); |
| | | } |
| | | }; |
| | | // 执行打印 |
| | | const executePrint = () => { |
| | | console.log('开始执行打印,数据条数:', printData.value.length); |
| | | console.log('打印数据:', printData.value); |
| | | |
| | | // 创建一个新的打印窗口 |
| | | const printWindow = window.open('', '_blank', 'width=800,height=600'); |
| | | |
| | | // 构建打印内容 |
| | | let printContent = ` |
| | | <!DOCTYPE html> |
| | | <html> |
| | | <head> |
| | | <meta charset="UTF-8"> |
| | | <title>打印预览</title> |
| | | <style> |
| | | body { |
| | | margin: 0; |
| | | padding: 0; |
| | | font-family: "SimSun", serif; |
| | | background: white; |
| | | } |
| | | .print-page { |
| | | width: 200mm; |
| | | height: 75mm; |
| | | padding: 10mm; |
| | | padding-left: 20mm; |
| | | background: white; |
| | | box-sizing: border-box; |
| | | page-break-after: always; |
| | | page-break-inside: avoid; |
| | | } |
| | | .print-page:last-child { |
| | | page-break-after: avoid; |
| | | } |
| | | .delivery-note { |
| | | width: 100%; |
| | | height: 100%; |
| | | font-size: 12px; |
| | | line-height: 1.2; |
| | | display: flex; |
| | | flex-direction: column; |
| | | color: #000; |
| | | } |
| | | .header { |
| | | text-align: center; |
| | | margin-bottom: 8px; |
| | | } |
| | | .company-name { |
| | | font-size: 18px; |
| | | font-weight: bold; |
| | | margin-bottom: 4px; |
| | | } |
| | | .document-title { |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | } |
| | | .info-section { |
| | | margin-bottom: 8px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | .info-row { |
| | | line-height: 20px; |
| | | } |
| | | .label { |
| | | font-weight: bold; |
| | | width: 60px; |
| | | font-size: 12px; |
| | | } |
| | | .value { |
| | | margin-right: 20px; |
| | | min-width: 80px; |
| | | font-size: 12px; |
| | | } |
| | | .table-section { |
| | | margin-bottom: 40px; |
| | | // flex: 0.6; |
| | | } |
| | | .product-table { |
| | | width: 100%; |
| | | border-collapse: collapse; |
| | | border: 1px solid #000; |
| | | } |
| | | .product-table th, .product-table td { |
| | | border: 1px solid #000; |
| | | padding: 6px; |
| | | text-align: center; |
| | | font-size: 12px; |
| | | line-height: 1.4; |
| | | } |
| | | .product-table th { |
| | | font-weight: bold; |
| | | } |
| | | .total-value { |
| | | font-weight: bold; |
| | | } |
| | | .footer-section { |
| | | margin-top: auto; |
| | | } |
| | | .footer-row { |
| | | display: flex; |
| | | margin-bottom: 3px; |
| | | line-height: 22px; |
| | | justify-content: space-between; |
| | | } |
| | | .footer-item { |
| | | display: flex; |
| | | margin-right: 20px; |
| | | } |
| | | .footer-item .label { |
| | | font-weight: bold; |
| | | width: 80px; |
| | | font-size: 12px; |
| | | } |
| | | .footer-item .value { |
| | | min-width: 80px; |
| | | font-size: 12px; |
| | | } |
| | | .address-item .address-value { |
| | | min-width: 200px; |
| | | } |
| | | @media print { |
| | | body { |
| | | margin: 0; |
| | | padding: 0; |
| | | } |
| | | .print-page { |
| | | margin: 0; |
| | | padding: 10mm; |
| | | /* padding-left: 20mm; */ |
| | | page-break-inside: avoid; |
| | | page-break-after: always; |
| | | } |
| | | .print-page:last-child { |
| | | page-break-after: avoid; |
| | | } |
| | | } |
| | | </style> |
| | | </head> |
| | | <body> |
| | | `; |
| | | |
| | | // 为每条数据生成打印页面 |
| | | printData.value.forEach((item, index) => { |
| | | printContent += ` |
| | | <div class="print-page"> |
| | | <div class="delivery-note"> |
| | | <div class="header"> |
| | | <div class="company-name">鼎诚瑞实业有限责任公司</div> |
| | | <div class="document-title">零售发货单</div> |
| | | </div> |
| | | |
| | | <div class="info-section"> |
| | | <div class="info-row"> |
| | | <div> |
| | | <span class="label">发货日期:</span> |
| | | <span class="value">${formatDate(item.createTime)}</span> |
| | | </div> |
| | | <div> |
| | | <span class="label">客户名称:</span> |
| | | <span class="value">${item.customerName || '张爱有'}</span> |
| | | </div> |
| | | </div> |
| | | <div class="info-row"> |
| | | <span class="label">单号:</span> |
| | | <span class="value">${item.salesContractNo || ''}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="table-section"> |
| | | <table class="product-table"> |
| | | <thead> |
| | | <tr> |
| | | <th>产品名称</th> |
| | | <th>规格型号</th> |
| | | <th>单位</th> |
| | | <th>单价</th> |
| | | <th>零售数量</th> |
| | | <th>零售金额</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | ${item.products && item.products.length > 0 ? |
| | | item.products.map(product => ` |
| | | <tr> |
| | | <td>${product.productCategory || ''}</td> |
| | | <td>${product.specificationModel || ''}</td> |
| | | <td>${product.unit || ''}</td> |
| | | <td>${product.taxInclusiveUnitPrice || '0'}</td> |
| | | <td>${product.quantity || '0'}</td> |
| | | <td>${product.taxInclusiveTotalPrice || '0'}</td> |
| | | </tr> |
| | | `).join('') : |
| | | '<tr><td colspan="6" style="text-align: center; color: #999;">暂无产品数据</td></tr>' |
| | | const handlePrintLabel = async () => { |
| | | if (selectedRows.value.length !== 1) { |
| | | proxy.$modal.msgWarning("请选择一条销售台账数据进行标签打印"); |
| | | return; |
| | | } |
| | | |
| | | const selectedId = selectedRows.value[0]?.id; |
| | | if (!selectedId) { |
| | | proxy.$modal.msgWarning("当前选择数据缺少ID,无法打印标签"); |
| | | return; |
| | | } |
| | | |
| | | proxy.$modal.loading("正在获取标签数据,请稍候..."); |
| | | try { |
| | | const res = await getSalesLabel(selectedId); |
| | | const labelList = res?.data ?? []; |
| | | if (!Array.isArray(labelList) || labelList.length === 0) { |
| | | proxy.$modal.msgWarning("暂无可打印标签数据"); |
| | | return; |
| | | } |
| | | </tbody> |
| | | <tfoot> |
| | | <tr> |
| | | <td class="label">合计</td> |
| | | <td class="total-value"></td> |
| | | <td class="total-value"></td> |
| | | <td class="total-value"></td> |
| | | <td class="total-value">${getTotalQuantityForPrint(item.products)}</td> |
| | | <td class="total-value">${getTotalAmountForPrint(item.products)}</td> |
| | | </tr> |
| | | </tfoot> |
| | | </table> |
| | | </div> |
| | | |
| | | <div class="footer-section"> |
| | | <div class="footer-row"> |
| | | <div class="footer-item"> |
| | | <span class="label">收货电话:</span> |
| | | <span class="value"></span> |
| | | </div> |
| | | <div class="footer-item"> |
| | | <span class="label">收货人:</span> |
| | | <span class="value"></span> |
| | | </div> |
| | | <div class="footer-item address-item"> |
| | | <span class="label">收货地址:</span> |
| | | <span class="value address-value"></span> |
| | | </div> |
| | | </div> |
| | | <div class="footer-row"> |
| | | <div class="footer-item"> |
| | | <span class="label">操作员:</span> |
| | | <span class="value">${userStore.nickName || '撕开前'}</span> |
| | | </div> |
| | | <div class="footer-item"> |
| | | <span class="label">打印日期:</span> |
| | | <span class="value">${formatDateTime(new Date())}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | `; |
| | | }); |
| | | |
| | | printContent += ` |
| | | </body> |
| | | </html> |
| | | `; |
| | | |
| | | // 写入内容到新窗口 |
| | | printWindow.document.write(printContent); |
| | | printWindow.document.close(); |
| | | |
| | | // 等待内容加载完成后打印 |
| | | printWindow.onload = () => { |
| | | setTimeout(() => { |
| | | printWindow.print(); |
| | | printWindow.close(); |
| | | printPreviewVisible.value = false; |
| | | }, 500); |
| | | }; |
| | | }; |
| | | // 格式化日期 |
| | | const formatDate = (dateString) => { |
| | | if (!dateString) return getCurrentDate(); |
| | | const date = new Date(dateString); |
| | | const year = date.getFullYear(); |
| | | const month = String(date.getMonth() + 1).padStart(2, "0"); |
| | | const day = String(date.getDate()).padStart(2, "0"); |
| | | return `${year}/${month}/${day}`; |
| | | }; |
| | | // 格式化日期时间 |
| | | const formatDateTime = (date) => { |
| | | const year = date.getFullYear(); |
| | | const month = String(date.getMonth() + 1).padStart(2, "0"); |
| | | const day = String(date.getDate()).padStart(2, "0"); |
| | | const hours = String(date.getHours()).padStart(2, "0"); |
| | | const minutes = String(date.getMinutes()).padStart(2, "0"); |
| | | const seconds = String(date.getSeconds()).padStart(2, "0"); |
| | | return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`; |
| | | }; |
| | | // 计算产品总数量 |
| | | const getTotalQuantity = (products) => { |
| | | if (!products || products.length === 0) return '0'; |
| | | const total = products.reduce((sum, product) => { |
| | | return sum + (parseFloat(product.quantity) || 0); |
| | | }, 0); |
| | | return total.toFixed(2); |
| | | }; |
| | | |
| | | // 计算产品总金额 |
| | | const getTotalAmount = (products) => { |
| | | if (!products || products.length === 0) return '0'; |
| | | const total = products.reduce((sum, product) => { |
| | | return sum + (parseFloat(product.taxInclusiveTotalPrice) || 0); |
| | | }, 0); |
| | | return total.toFixed(2); |
| | | }; |
| | | |
| | | // 用于打印的计算函数 |
| | | const getTotalQuantityForPrint = (products) => { |
| | | if (!products || products.length === 0) return '0'; |
| | | const total = products.reduce((sum, product) => { |
| | | return sum + (parseFloat(product.quantity) || 0); |
| | | }, 0); |
| | | return total.toFixed(2); |
| | | }; |
| | | |
| | | const getTotalAmountForPrint = (products) => { |
| | | if (!products || products.length === 0) return '0'; |
| | | const total = products.reduce((sum, product) => { |
| | | return sum + (parseFloat(product.taxInclusiveTotalPrice) || 0); |
| | | }, 0); |
| | | return total.toFixed(2); |
| | | printSalesLabel(labelList); |
| | | } catch (error) { |
| | | console.error("打印标签失败:", error); |
| | | proxy.$modal.msgError("打印标签失败,请稍后重试"); |
| | | } finally { |
| | | proxy.$modal.closeLoading(); |
| | | } |
| | | }; |
| | | |
| | | const mathNum = () => { |
| | |
| | | if (!productForm.value.quantity) { |
| | | return; |
| | | } |
| | | // 含税总价计算 |
| | | const settlePieceArea = parseFloat(productForm.value.settlePieceArea) || 1; |
| | | // 含税总价计算 = 单价 * 结算面积 * 数量 |
| | | productForm.value.taxInclusiveTotalPrice = |
| | | proxy.calculateTaxIncludeTotalPrice( |
| | | productForm.value.taxInclusiveUnitPrice, |
| | | productForm.value.taxInclusiveUnitPrice * settlePieceArea, |
| | | productForm.value.quantity |
| | | ); |
| | | if (productForm.value.taxRate) { |
| | |
| | | productForm.value.taxRate |
| | | ); |
| | | } |
| | | }; |
| | | |
| | | // 新增:尺寸(宽高)与面积(单片/总计)联动 |
| | | const recalcAreaTotals = () => { |
| | | const qty = Number(productForm.value.quantity ?? 0) || 0; |
| | | const actualPiece = Number(productForm.value.actualPieceArea ?? 0) || 0; |
| | | const settlePiece = Number(productForm.value.settlePieceArea ?? 0) || 0; |
| | | |
| | | productForm.value.actualTotalArea = Number((actualPiece * qty).toFixed(5)); |
| | | productForm.value.settleTotalArea = Number((settlePiece * qty).toFixed(5)); |
| | | }; |
| | | |
| | | // 新增:周长(cm)(重箱heavyBox周长) |
| | | // width/height 单位为 mm,因此周长(cm)需要除以 10 |
| | | const recalcPerimeterFromWidthHeight = () => { |
| | | const width = Number(productForm.value.width ?? 0) || 0; |
| | | const height = Number(productForm.value.height ?? 0) || 0; |
| | | |
| | | if (width <= 0 || height <= 0) { |
| | | productForm.value.perimeter = 0; |
| | | return; |
| | | } |
| | | |
| | | // 周长 = (宽 + 高) * 2,单位从 mm 转为 cm:/10 |
| | | productForm.value.perimeter = Number((((width + height) * 2) / 10).toFixed(2)); |
| | | }; |
| | | |
| | | const recalcAreaFromWidthHeight = () => { |
| | | const width = Number(productForm.value.width ?? 0) || 0; |
| | | const height = Number(productForm.value.height ?? 0) || 0; |
| | | |
| | | if (width <= 0 || height <= 0) { |
| | | // 宽高为空/为0时,把单片面积与总面积置为0 |
| | | productForm.value.actualPieceArea = 0; |
| | | productForm.value.actualTotalArea = 0; |
| | | productForm.value.perimeter = 0; |
| | | |
| | | // 只有在结算单片面积也为空/为0时,才同步置0,避免覆盖用户手动填写 |
| | | const settlePiece = Number(productForm.value.settlePieceArea ?? 0) || 0; |
| | | if (!settlePiece) { |
| | | productForm.value.settlePieceArea = 0; |
| | | } |
| | | productForm.value.settleTotalArea = Number( |
| | | ((Number(productForm.value.settlePieceArea ?? 0) || 0) * (Number(productForm.value.quantity ?? 0) || 0)).toFixed(5) |
| | | ); |
| | | return; |
| | | } |
| | | |
| | | const computedPieceArea = (width * height) / 1e6; // mm*mm -> ㎡ |
| | | const computed = Number(computedPieceArea.toFixed(5)); |
| | | |
| | | productForm.value.actualPieceArea = computed; |
| | | productForm.value.settlePieceArea = computed; |
| | | |
| | | recalcPerimeterFromWidthHeight(); |
| | | recalcAreaTotals(); |
| | | // 面积更新后,重新计算含税总价 = 单价 * 结算面积 * 数量 |
| | | calculateFromUnitPrice(true); |
| | | }; |
| | | |
| | | // 根据含税总价计算含税单价和数量 |
| | |
| | | return; |
| | | } |
| | | if (isCalculating.value) return; |
| | | |
| | | |
| | | const quantity = parseFloat(productForm.value.quantity); |
| | | const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice); |
| | | |
| | | const settlePieceArea = parseFloat(productForm.value.settlePieceArea) || 1; |
| | | |
| | | if (!quantity || quantity <= 0 || !unitPrice) { |
| | | return; |
| | | } |
| | | |
| | | |
| | | isCalculating.value = true; |
| | | |
| | | // 计算含税总价 |
| | | productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2); |
| | | |
| | | |
| | | // 计算含税总价 = 单价 * 结算面积 * 数量 |
| | | productForm.value.taxInclusiveTotalPrice = (unitPrice * settlePieceArea * quantity).toFixed(2); |
| | | |
| | | // 如果有税率,计算不含税总价 |
| | | if (productForm.value.taxRate) { |
| | | productForm.value.taxExclusiveTotalPrice = |
| | |
| | | productForm.value.taxRate |
| | | ); |
| | | } |
| | | |
| | | |
| | | isCalculating.value = false; |
| | | }; |
| | | |
| | | // 根据含税单价变化计算总价 |
| | | const calculateFromUnitPrice = () => { |
| | | const calculateFromUnitPrice = (silent = false) => { |
| | | if (!productForm.value.taxRate) { |
| | | proxy.$modal.msgWarning("请先选择税率"); |
| | | if (!silent) proxy.$modal.msgWarning("请先选择税率"); |
| | | return; |
| | | } |
| | | if (isCalculating.value) return; |
| | | |
| | | |
| | | const quantity = parseFloat(productForm.value.quantity); |
| | | const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice); |
| | | |
| | | const settlePieceArea = parseFloat(productForm.value.settlePieceArea) || 1; |
| | | |
| | | if (!quantity || quantity <= 0 || !unitPrice) { |
| | | return; |
| | | } |
| | | |
| | | |
| | | isCalculating.value = true; |
| | | |
| | | // 计算含税总价 |
| | | productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2); |
| | | |
| | | |
| | | // 计算含税总价 = 单价 * 结算面积 * 数量 |
| | | productForm.value.taxInclusiveTotalPrice = (unitPrice * settlePieceArea * quantity).toFixed(2); |
| | | |
| | | // 如果有税率,计算不含税总价 |
| | | if (productForm.value.taxRate) { |
| | | productForm.value.taxExclusiveTotalPrice = |
| | |
| | | productForm.value.taxRate |
| | | ); |
| | | } |
| | | |
| | | |
| | | isCalculating.value = false; |
| | | }; |
| | | |
| | |
| | | return statusStr === '待发货' || statusStr === '审核拒绝'; |
| | | }; |
| | | |
| | | const handleBulkDelivery = async () => { |
| | | if (selectedRows.value.length === 0) { |
| | | proxy.$modal.msgWarning("请选择数据"); |
| | | return; |
| | | } |
| | | |
| | | const customerNames = selectedRows.value.map((r) => String(r.customerName || "").trim()); |
| | | const uniqueCustomers = Array.from(new Set(customerNames)); |
| | | |
| | | // 客户名称不一致不允许发货 |
| | | if (uniqueCustomers.length > 1) { |
| | | proxy.$modal.msgWarning("客户名称不一致,不允许发货"); |
| | | return; |
| | | } |
| | | |
| | | // 多条且客户一致:二次确认 |
| | | if (selectedRows.value.length > 1) { |
| | | try { |
| | | await ElMessageBox.confirm("是否确认合并发货?", "合并发货", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | }); |
| | | } catch (e) { |
| | | proxy.$modal.msg("已取消"); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | proxy.$modal.loading("正在获取产品数据,请稍候..."); |
| | | try { |
| | | const targets = []; |
| | | for (const ledger of selectedRows.value) { |
| | | let products = []; |
| | | try { |
| | | const res = await productList({ salesLedgerId: ledger.id, type: 1 }); |
| | | products = res?.data || []; |
| | | } catch { |
| | | products = []; |
| | | } |
| | | |
| | | for (const product of products) { |
| | | if (!canShip(product)) continue; |
| | | targets.push({ |
| | | ...product, |
| | | salesLedgerId: product.salesLedgerId || ledger.id, |
| | | }); |
| | | } |
| | | } |
| | | |
| | | if (targets.length === 0) { |
| | | proxy.$modal.msgWarning("没有可发货的数据"); |
| | | return; |
| | | } |
| | | |
| | | currentDeliveryRows.value = targets; |
| | | deliveryForm.value = { type: "货车" }; |
| | | // 重置审批人节点(默认一个空节点) |
| | | approverNodes.value = [{ id: 1, userId: null }]; |
| | | nextApproverId = 2; |
| | | deliveryFormVisible.value = true; |
| | | } finally { |
| | | proxy.$modal.closeLoading(); |
| | | } |
| | | }; |
| | | |
| | | /** |
| | | * 下载文件 |
| | | * |
| | |
| | | return; |
| | | } |
| | | |
| | | currentDeliveryRow.value = row; |
| | | currentDeliveryRows.value = [row]; |
| | | deliveryForm.value = { |
| | | type: "货车", |
| | | }; |
| | |
| | | const approveUserIds = approverNodes.value.map(node => node.userId).join(","); |
| | | // 保存当前展开的行ID,以便发货后重新加载子表格数据 |
| | | const currentExpandedKeys = [...expandedRowKeys.value]; |
| | | const salesLedgerId = currentDeliveryRow.value.salesLedgerId; |
| | | addShippingInfo({ |
| | | salesLedgerId: salesLedgerId, |
| | | salesLedgerProductId: currentDeliveryRow.value.id, |
| | | type: deliveryForm.value.type, |
| | | approveUserIds, |
| | | }) |
| | | |
| | | const targets = currentDeliveryRows.value || []; |
| | | if (targets.length === 0) { |
| | | proxy.$modal.msgWarning("未选择可发货的数据"); |
| | | return; |
| | | } |
| | | |
| | | // 依次发货(避免并发下库存扣减/状态更新互相影响) |
| | | const run = async () => { |
| | | for (const item of targets) { |
| | | const salesLedgerId = item.salesLedgerId; |
| | | if (!salesLedgerId) continue; |
| | | await addShippingInfo({ |
| | | salesLedgerId, |
| | | salesLedgerProductId: item.id, |
| | | type: deliveryForm.value.type, |
| | | approveUserIds, |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | run() |
| | | .then(() => { |
| | | proxy.$modal.msgSuccess("发货成功"); |
| | | closeDeliveryDia(); |
| | |
| | | getList().then(() => { |
| | | // 如果之前有展开的行,重新加载这些行的子表格数据 |
| | | if (currentExpandedKeys.length > 0) { |
| | | // 使用 Promise.all 并行加载所有展开行的子表格数据 |
| | | const loadPromises = currentExpandedKeys.map(ledgerId => { |
| | | const loadPromises = currentExpandedKeys.map((ledgerId) => { |
| | | return productList({ salesLedgerId: ledgerId, type: 1 }).then((res) => { |
| | | const index = tableData.value.findIndex((item) => item.id === ledgerId); |
| | | if (index > -1) { |
| | |
| | | }); |
| | | }); |
| | | Promise.all(loadPromises).then(() => { |
| | | // 恢复展开状态 |
| | | expandedRowKeys.value = currentExpandedKeys; |
| | | }); |
| | | } |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msgError("发货失败,请稍后重试"); |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | |
| | | const closeDeliveryDia = () => { |
| | | proxy.resetForm("deliveryFormRef"); |
| | | deliveryFormVisible.value = false; |
| | | currentDeliveryRow.value = null; |
| | | currentDeliveryRows.value = []; |
| | | }; |
| | | const currentFactoryName = ref(""); |
| | | const getCurrentFactoryName = async () => { |
| | |
| | | } |
| | | |
| | | ::v-deep .red { |
| | | background-color: #f80202; |
| | | background-color: #FAE1DE; |
| | | } |
| | | |
| | | ::v-deep .purple{ |
| | | background-color: #F4DEFA; |
| | | } |
| | | |
| | | .other-amount-select { |
| | | /* 多选标签区域强制单行,不让输入框随标签换行而拉高高度 */ |
| | | :deep .el-select__tags { |
| | | display: flex; |
| | | flex-wrap: nowrap !important; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | max-height: 32px; |
| | | } |
| | | } |
| | | |
| | | .table_list { |
| | |
| | | display: flex; |
| | | justify-content: space-between; |
| | | margin-bottom: 10px; |
| | | } |
| | | .print-preview-dialog { |
| | | .el-dialog__body { |
| | | padding: 0; |
| | | max-height: 80vh; |
| | | overflow-y: auto; |
| | | } |
| | | } |
| | | |
| | | .print-preview-container { |
| | | .print-preview-header { |
| | | padding: 15px; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | text-align: center; |
| | | |
| | | .el-button { |
| | | margin: 0 10px; |
| | | } |
| | | } |
| | | |
| | | .print-preview-content { |
| | | padding: 20px; |
| | | background-color: #f5f5f5; |
| | | min-height: 400px; |
| | | } |
| | | } |
| | | |
| | | .print-page { |
| | | width: 220mm; |
| | | height: 90mm; |
| | | padding: 10mm; |
| | | margin: 0 auto; |
| | | background: white; |
| | | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); |
| | | margin-bottom: 10px; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .delivery-note { |
| | | width: 100%; |
| | | height: 100%; |
| | | font-family: "SimSun", serif; |
| | | font-size: 10px; |
| | | line-height: 1.2; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .header { |
| | | text-align: center; |
| | | margin-bottom: 8px; |
| | | |
| | | .company-name { |
| | | font-size: 18px; |
| | | font-weight: bold; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .document-title { |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | } |
| | | } |
| | | |
| | | .info-section { |
| | | margin-bottom: 8px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | |
| | | .info-row { |
| | | line-height: 20px; |
| | | |
| | | .label { |
| | | font-weight: bold; |
| | | width: 60px; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .value { |
| | | margin-right: 20px; |
| | | min-width: 80px; |
| | | font-size: 14px; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .table-section { |
| | | margin-bottom: 4px; |
| | | flex: 1; |
| | | |
| | | .product-table { |
| | | width: 100%; |
| | | border-collapse: collapse; |
| | | border: 1px solid #000; |
| | | |
| | | th, td { |
| | | border: 1px solid #000; |
| | | padding: 6px; |
| | | text-align: center; |
| | | font-size: 14px; |
| | | line-height: 1.4; |
| | | } |
| | | |
| | | th { |
| | | font-weight: bold; |
| | | } |
| | | |
| | | .total-label { |
| | | text-align: right; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | .total-value { |
| | | font-weight: bold; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .footer-section { |
| | | .footer-row { |
| | | display: flex; |
| | | margin-bottom: 3px; |
| | | line-height: 20px; |
| | | justify-content: space-between; |
| | | |
| | | .footer-item { |
| | | display: flex; |
| | | margin-right: 20px; |
| | | |
| | | .label { |
| | | font-weight: bold; |
| | | width: 80px; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .value { |
| | | min-width: 80px; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | &.address-item { |
| | | .address-value { |
| | | min-width: 200px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | @media print { |
| | | .app-container { |
| | | display: none; |
| | | } |
| | | |
| | | .print-page { |
| | | box-shadow: none; |
| | | margin: 0; |
| | | padding: 10mm; |
| | | padding-left: 20mm; |
| | | page-break-inside: avoid; |
| | | page-break-after: always; |
| | | } |
| | | .print-page:last-child { |
| | | page-break-after: avoid; |
| | | } |
| | | } |
| | | </style> |