Merge remote-tracking branch 'origin/dev_pro_河南鹤壁' into dev_pro_河南鹤壁
# Conflicts:
# multiple/config.json
| | |
| | | "DDGW": { |
| | | "env": { |
| | | "VITE_APP_TITLE": "æ²³åä¸å¤§é«æ¸©èè½æææéå
¬å¸", |
| | | "VITE_BASE_API": "http://103.189.141.42:9001", |
| | | "VITE_JAVA_API": "http://103.189.141.42:9002" |
| | | "VITE_BASE_API": "http://36.133.40.190:81", |
| | | "VITE_JAVA_API": "http://36.133.40.190:82" |
| | | }, |
| | | "logo": "logo/DDGWLogo.png", |
| | | "favicon": "favicon/DDGWfavicon.ico" |
| | | }, |
| | | "DDGW-TEST": { |
| | | "env": { |
| | | "VITE_APP_TITLE": "æ²³åä¸å¤§é«æ¸©èè½æææéå
¬å¸", |
| | | "VITE_BASE_API": "http://36.133.40.190:83", |
| | | "VITE_JAVA_API": "http://36.133.40.190:84" |
| | | }, |
| | | "logo": "logo/DDGWLogo.png", |
| | | "favicon": "favicon/DDGWfavicon.ico" |
| | |
| | | }, |
| | | "logo": "logo/XHZGLogo.png", |
| | | "favicon": "favicon/XHZGfavicon.ico" |
| | | }, |
| | | "SDKS": { |
| | | "env": { |
| | | "VITE_APP_TITLE": "鹤å£å¸åè¾¾ç¿å±±è®¾å¤æéå
¬å¸", |
| | | "VITE_BASE_API": "http://36.213.180.217:9520", |
| | | "VITE_JAVA_API": "http://36.213.180.217:9521" |
| | | }, |
| | | "logo": "logo/SDKSLogo.png", |
| | | "favicon": "favicon/SDKSfavicon.ico" |
| | | }, |
| | | "JMTM": { |
| | | "env": { |
| | | "VITE_APP_TITLE": "æ²³åæ·ç¾ç¹éç§ææéå
¬å¸", |
| | | "VITE_BASE_API": "http://36.133.40.61:81", |
| | | "VITE_JAVA_API": "http://36.133.40.61:82" |
| | | }, |
| | | "logo": "logo/JMTMLogo.png", |
| | | "favicon": "favicon/JMTMfavicon.ico" |
| | | }, |
| | | "CHMY": { |
| | | "env": { |
| | | "VITE_APP_TITLE": "鹤壿å®é䏿éå
¬å¸", |
| | | "VITE_BASE_API": "http://36.133.39.224:81", |
| | | "VITE_JAVA_API": "http://36.133.39.224:82" |
| | | }, |
| | | "logo": "logo/CHMYLogo.png", |
| | | "favicon": "favicon/CHMYfavicon.ico" |
| | | }, |
| | | "SJDQ": { |
| | | "env": { |
| | | "VITE_APP_TITLE": "鹤å£å¸å±±ååºçä¹
çµå¨æéå
¬å¸", |
| | | "VITE_BASE_API": "http://36.139.201.235:81", |
| | | "VITE_JAVA_API": "http://36.139.201.235:82" |
| | | }, |
| | | "logo": "logo/SJDQLogo.png", |
| | | "favicon": "favicon/SJDQfavicon.ico" |
| | | }, |
| | | "TXDQ": { |
| | | "env": { |
| | | "VITE_APP_TITLE": "鹤壿³°ä¿¡çµæ°æéå
¬å¸", |
| | | "VITE_BASE_API": "http://36.133.46.171:81", |
| | | "VITE_JAVA_API": "http://36.133.46.171:82" |
| | | }, |
| | | "logo": "logo/TXDQLogo.png", |
| | | "favicon": "favicon/TXDQfavicon.ico" |
| | | }, |
| | | "HMCL": { |
| | | "env": { |
| | | "VITE_APP_TITLE": "鹤壿µ©éæææéå
¬å¸", |
| | | "VITE_BASE_API": "http://36.213.90.123:9522", |
| | | "VITE_JAVA_API": "http://36.213.90.123:9523" |
| | | }, |
| | | "logo": "logo/HMCLLogo.png", |
| | | "favicon": "favicon/HMCLfavicon.ico" |
| | | }, |
| | | "DTZG": { |
| | | "env": { |
| | |
| | | }, |
| | | "logo": "logo/XLZNLogo.png", |
| | | "favicon": "favicon/XLZNfavicon.ico" |
| | | },"SKJS": { |
| | | }, |
| | | "SKJS": { |
| | | "env": { |
| | | "VITE_APP_TITLE": "å¾å·ä»å
éå±å¶åæéå
¬å¸", |
| | | "VITE_BASE_API": "http://36.213.90.123:9514", |
| | |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | export function listAll() { |
| | | return request({ |
| | | url: "/technologyOperation/listPage", |
| | | method: "get", |
| | | params: { current: -1, size: -1 }, |
| | | }); |
| | | } |
| | | export function add(data) { |
| | | return request({ |
| | | url: "/technologyOperation/add", |
| | |
| | | data: query, |
| | | }); |
| | | } |
| | | |
| | | // æ¥è¯¢ä¸åæ ¼è¯¦æ
|
| | | export function getQualityUnqualifiedDetail(query) { |
| | | return request({ |
| | | url: "/quality/qualityUnqualified/detail/" + query, |
| | | method: "get", |
| | | data: query, |
| | | }); |
| | | } |
| | |
| | | <template> |
| | | <el-dialog v-model="dialogVisible" title="é¢è§" width="100%" fullscreen align-center :before-close="handleClose" append-to-body> |
| | | <el-dialog v-model="dialogVisible" title="é¢è§" :width="isImage ? '60%' : '100%'" :fullscreen="!isImage" align-center :before-close="handleClose" append-to-body> |
| | | <div> |
| | | <!-- å¾çé¢è§ --> |
| | | <div v-if="isImage"> |
| | | <div v-if="isImage" class="image-preview-container"> |
| | | <img :src="imgUrl" alt="Image Preview" /> |
| | | </div> |
| | | |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .image-preview-container { |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | max-height: calc(100vh - 200px); |
| | | width: 100%; |
| | | } |
| | | |
| | | img { |
| | | max-width: 100%; |
| | | max-height: calc(100vh - 200px); |
| | | object-fit: contain; |
| | | display: block; |
| | | margin: 0 auto; |
| | | } |
| | |
| | | <template> |
| | | <el-dialog v-model="visible" title="éæ©äº§å" width="900px" destroy-on-close :close-on-click-modal="false"> |
| | | <el-form :inline="true" :model="query" class="mb-2"> |
| | | <el-form-item label="产å大类"> |
| | | <el-select v-model="query.topProductParentId" placeholder="éæ©äº§å大类" clearable @change="onSearch" style="width: 150px"> |
| | | <el-option v-for="item in categoryList" :key="item.id" :label="item.label || item.productName" :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="产ååç§°"> |
| | | <el-input v-model="query.productName" placeholder="è¾å
¥äº§ååç§°" clearable @keyup.enter="onSearch" style="width: 200px" /> |
| | | <el-input v-model="query.productName" placeholder="è¾å
¥äº§ååç§°" clearable @keyup.enter="onSearch" style="width: 150px" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="产ååå·"> |
| | | <el-input v-model="query.model" placeholder="è¾å
¥äº§ååå·" clearable @keyup.enter="onSearch" style="width: 200px" /> |
| | | <el-input v-model="query.model" placeholder="è¾å
¥äº§ååå·" clearable @keyup.enter="onSearch" style="width: 150px" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item> |
| | |
| | | import { computed, onMounted, reactive, ref, watch, nextTick } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { productModelList, productModelListByUrl } from '@/api/basicData/productModel' |
| | | import { productTreeList } from '@/api/basicData/product' |
| | | |
| | | export type ProductRow = { |
| | | id: number; |
| | |
| | | const query = reactive({ |
| | | productName: "", |
| | | model: "", |
| | | topProductParentId: undefined as number | undefined, |
| | | }); |
| | | |
| | | const categoryList = ref<any[]>([]); |
| | | |
| | | async function loadCategory() { |
| | | try { |
| | | const res = await productTreeList(); |
| | | categoryList.value = res || []; |
| | | } catch (e) { |
| | | console.error("Failed to load product categories", e); |
| | | } |
| | | } |
| | | |
| | | const page = reactive({ |
| | | pageNum: 1, |
| | |
| | | function onReset() { |
| | | query.productName = ""; |
| | | query.model = ""; |
| | | query.topProductParentId = props.topProductParentId; |
| | | page.pageNum = 1; |
| | | loadData(); |
| | | } |
| | |
| | | model: query.model.trim(), |
| | | current: page.pageNum, |
| | | size: page.pageSize, |
| | | topProductParentId: props.topProductParentId, |
| | | topProductParentId: query.topProductParentId ?? props.topProductParentId, |
| | | }; |
| | | const res: any = props.requestUrl |
| | | ? await productModelListByUrl(props.requestUrl, params) |
| | |
| | | watch(() => props.modelValue, (visible) => { |
| | | if (visible) { |
| | | multipleSelection.value = []; |
| | | query.topProductParentId = props.topProductParentId; |
| | | } |
| | | }); |
| | | |
| | | onMounted(() => { |
| | | loadData() |
| | | query.topProductParentId = props.topProductParentId; |
| | | loadCategory(); |
| | | loadData(); |
| | | }) |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog v-model="dialogFormVisible" |
| | | :title="operationType === 'approval' ? '审æ¹' : '详æ
'" |
| | | width="700px" |
| | | @close="closeDia"> |
| | | <el-descriptions border :column="1" class="approval-detail"> |
| | | <el-descriptions-item label="æµç¨ç¼å·">{{ form.approveId }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç³è¯·é¨é¨">{{ form.approveDeptName }}</el-descriptions-item> |
| | | <el-descriptions-item :label="props.approveType == 5 ? 'éè´ååå·' : '审æ¹äºç±'" v-if="!isQuotationApproval && !isPurchaseApproval"> |
| | | {{ form.approveReason }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="æ¥ééé¢" v-if="Number(props.approveType) === 4"> |
| | | {{ form.price }} |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | <!-- æ¥ä»·å®¡æ¹ï¼å±ç¤ºæ¥ä»·è¯¦æ
ï¼å¤ç¨é宿¥ä»·"æ¥ç详æ
å¯¹è¯æ¡"å
å®¹ç»æï¼ --> |
| | | <div v-if="isQuotationApproval" |
| | | style="margin: 10px 0 18px;"> |
| | | <el-divider content-position="left">æ¥ä»·è¯¦æ
</el-divider> |
| | | <el-skeleton :loading="quotationLoading" |
| | | animated> |
| | | <template #template> |
| | | <el-skeleton-item variant="h3" |
| | | style="width: 30%" /> |
| | | <el-skeleton-item variant="text" |
| | | style="width: 100%" /> |
| | | <el-skeleton-item variant="text" |
| | | style="width: 100%" /> |
| | | </template> |
| | | <template #default> |
| | | <el-empty v-if="!currentQuotation || !currentQuotation.quotationNo" |
| | | description="æªæ¥è¯¢å°å¯¹åºæ¥ä»·è¯¦æ
" /> |
| | | <template v-else> |
| | | <el-descriptions :column="2" |
| | | border> |
| | | <el-descriptions-item label="æ¥ä»·åå·">{{ currentQuotation.quotationNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="客æ·åç§°">{{ currentQuotation.customer }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä¸å¡å">{{ currentQuotation.salesperson }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ¥ä»·æ¥æ">{{ currentQuotation.quotationDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="æææè³">{{ currentQuotation.validDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="仿¬¾æ¹å¼">{{ currentQuotation.paymentMethod }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ¥ä»·æ»é¢" |
| | | :span="2"> |
| | | <span style="font-size: 18px; color: #e6a23c; font-weight: bold;"> |
| | | {{ formatCurrency(currentQuotation.totalAmount) }} |
| | | </span> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | <div style="margin-top: 20px;"> |
| | | <h4>产åæç»</h4> |
| | | <el-table :data="currentQuotation.products || []" |
| | | border |
| | | style="width: 100%"> |
| | | <el-table-column prop="product" |
| | | label="产ååç§°" /> |
| | | <el-table-column prop="specification" |
| | | label="è§æ ¼åå·" /> |
| | | <el-table-column prop="unit" |
| | | label="åä½" /> |
| | | <el-table-column prop="unitPrice" |
| | | label="åä»·"> |
| | | <template #default="scope">{{ formatCurrency(scope.row.unitPrice) }}</template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | <div v-if="currentQuotation.remark" |
| | | style="margin-top: 20px;"> |
| | | <h4>夿³¨</h4> |
| | | <p>{{ currentQuotation.remark }}</p> |
| | | </div> |
| | | </template> |
| | | </template> |
| | | </el-skeleton> |
| | | </div> |
| | | <!-- éè´å®¡æ¹ï¼å±ç¤ºéè´è¯¦æ
--> |
| | | <div v-if="isPurchaseApproval" |
| | | style="margin: 10px 0 18px;"> |
| | | <el-divider content-position="left">éè´è¯¦æ
</el-divider> |
| | | <el-skeleton :loading="purchaseLoading" |
| | | animated> |
| | | <template #template> |
| | | <el-skeleton-item variant="h3" |
| | | style="width: 30%" /> |
| | | <el-skeleton-item variant="text" |
| | | style="width: 100%" /> |
| | | <el-skeleton-item variant="text" |
| | | style="width: 100%" /> |
| | | </template> |
| | | <template #default> |
| | | <el-empty v-if="!currentPurchase || !currentPurchase.purchaseContractNumber" |
| | | description="æªæ¥è¯¢å°å¯¹åºéè´è¯¦æ
" /> |
| | | <template v-else> |
| | | <el-descriptions :column="2" |
| | | border> |
| | | <el-descriptions-item label="éè´ååå·">{{ currentPurchase.purchaseContractNumber }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä¾åºååç§°">{{ currentPurchase.supplierName }}</el-descriptions-item> |
| | | <el-descriptions-item label="项ç®åç§°">{{ currentPurchase.projectName }}</el-descriptions-item> |
| | | <el-descriptions-item label="éå®ååå·">{{ currentPurchase.salesContractNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç¾è®¢æ¥æ">{{ currentPurchase.executionDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="å½å
¥æ¥æ">{{ currentPurchase.entryDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="仿¬¾æ¹å¼">{{ currentPurchase.paymentMethod }}</el-descriptions-item> |
| | | <el-descriptions-item label="ååéé¢" |
| | | :span="2"> |
| | | <span style="font-size: 18px; color: #e6a23c; font-weight: bold;"> |
| | | {{ formatCurrency(currentPurchase.contractAmount) }} |
| | | </span> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | <div style="margin-top: 20px;"> |
| | | <h4>产åæç»</h4> |
| | | <el-table :data="currentPurchase.productData || []" |
| | | border |
| | | style="width: 100%"> |
| | | <el-table-column prop="productCategory" |
| | | label="产ååç§°" /> |
| | | <el-table-column prop="specificationModel" |
| | | label="è§æ ¼åå·" /> |
| | | <el-table-column prop="unit" |
| | | label="åä½" /> |
| | | <el-table-column prop="quantity" |
| | | label="æ°é" /> |
| | | <el-table-column prop="taxInclusiveUnitPrice" |
| | | label="å«ç¨åä»·"> |
| | | <template #default="scope">{{ formatCurrency(scope.row.taxInclusiveUnitPrice) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="taxInclusiveTotalPrice" |
| | | label="å«ç¨æ»ä»·"> |
| | | <template #default="scope">{{ formatCurrency(scope.row.taxInclusiveTotalPrice) }}</template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </template> |
| | | </template> |
| | | </el-skeleton> |
| | | </div> |
| | | <!-- å货审æ¹ï¼å±ç¤ºå货详æ
--> |
| | | <div v-if="isDeliveryApproval" |
| | | style="margin: 10px 0 18px;"> |
| | | <el-divider content-position="left">å货详æ
</el-divider> |
| | | <el-skeleton :loading="deliveryLoading" |
| | | animated> |
| | | <template #template> |
| | | <el-skeleton-item variant="h3" |
| | | style="width: 30%" /> |
| | | <el-skeleton-item variant="text" |
| | | style="width: 100%" /> |
| | | <el-skeleton-item variant="text" |
| | | style="width: 100%" /> |
| | | </template> |
| | | <template #default> |
| | | <el-empty v-if="!currentDelivery || !currentDelivery.shippingInfo" |
| | | description="æªæ¥è¯¢å°å¯¹åºå货详æ
" /> |
| | | <template v-else> |
| | | <el-descriptions :column="2" |
| | | border> |
| | | <el-descriptions-item label="éå®è®¢å">{{ currentDelivery.shippingInfo.salesContractNo || '--' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å货订åå·">{{ currentDelivery.shippingInfo.shippingNo || '--' }}</el-descriptions-item> |
| | | <el-descriptions-item label="客æ·åç§°">{{ currentDelivery.shippingInfo.customerName || '--' }}</el-descriptions-item> |
| | | <el-descriptions-item label="åè´§ç±»å">{{ currentDelivery.shippingInfo.type || '--' }}</el-descriptions-item> |
| | | <el-descriptions-item label="åè´§æ¥æ">{{ currentDelivery.shippingInfo.shippingDate || '--' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å®¡æ ¸ç¶æ">{{ currentDelivery.shippingInfo.status || '--' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å货车çå·">{{ currentDelivery.shippingInfo.shippingCarNumber || '--' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å¿«éå
¬å¸">{{ currentDelivery.shippingInfo.expressCompany || '--' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å¿«éåå·" |
| | | :span="2">{{ currentDelivery.shippingInfo.expressNumber || '--' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | <div style="margin-top: 20px;"> |
| | | <h4>产åæç»</h4> |
| | | <el-table :data="deliveryProductList" |
| | | border |
| | | size="small" |
| | | style="width: 100%"> |
| | | <el-table-column prop="batchNo" |
| | | label="æ¹å·" |
| | | show-overflow-tooltip /> |
| | | <el-table-column prop="productName" |
| | | label="产ååç§°" |
| | | show-overflow-tooltip /> |
| | | <el-table-column prop="specificationModel" |
| | | label="è§æ ¼åå·" |
| | | show-overflow-tooltip /> |
| | | <el-table-column prop="deliveryQuantity" |
| | | label="åè´§æ°é" |
| | | align="center" /> |
| | | </el-table> |
| | | </div> |
| | | <div v-if="currentDelivery.shippingInfo.storageBlobVOs && currentDelivery.shippingInfo.storageBlobVOs.length" |
| | | style="margin-top: 20px;"> |
| | | <h4>åè´§å¾ç</h4> |
| | | <ImagePreview :file-list="currentDelivery.shippingInfo.storageBlobVOs" /> |
| | | </div> |
| | | </template> |
| | | </template> |
| | | </el-skeleton> |
| | | </div> |
| | | <div class="activities-wrapper" style="margin-top: 20px;"> |
| | | <el-steps :active="getActiveStep()" |
| | | finish-status="success" |
| | | process-status="process" |
| | | align-center |
| | | direction="vertical"> |
| | | <el-step v-for="(activity, index) in activities" |
| | | :key="index" |
| | | finish-status="success" |
| | | :title="getNodeTitle(index, activities.length)" |
| | | :description="activity.approveNodeUser" |
| | | :icon="getNodeIcon(activity, index)"> |
| | | <template #icon> |
| | | <el-icon v-if="activity.approveNodeStatus === 2" |
| | | color="red" |
| | | :size="22"> |
| | | <WarningFilled /> |
| | | </el-icon> |
| | | <el-icon v-else-if="activity.isShen" |
| | | color="#1890ff" |
| | | :size="22"> |
| | | <Edit /> |
| | | </el-icon> |
| | | <el-icon v-else-if="activity.approveNodeStatus === 1" |
| | | color="#67C23A" |
| | | :size="26"> |
| | | <Check /> |
| | | </el-icon> |
| | | <el-icon v-else |
| | | color="#C0C4CC" |
| | | :size="22"> |
| | | <MoreFilled /> |
| | | </el-icon> |
| | | </template> |
| | | <template #title> |
| | | <span style="color: #000000">{{ getNodeTitle(index, activities.length) }}</span> |
| | | </template> |
| | | <template #description> |
| | | <div class="node-user"> |
| | | <div class="avatar-wrapper"> |
| | | <img :src="userStore.avatar" |
| | | class="user-avatar" |
| | | alt="" /> |
| | | </div> |
| | | <span style="color: #000000">{{ activity.approveNodeUser }}</span> |
| | | </div> |
| | | <div class="node-reason" v-if="activity.approveNodeReason"> |
| | | <span>å®¡æ¹æè§ï¼</span>{{ activity.approveNodeReason }} |
| | | </div> |
| | | </template> |
| | | </el-step> |
| | | </el-steps> |
| | | </div> |
| | | |
| | | <el-descriptions border :column="1" style="margin-top: 20px;" class="approval-detail"> |
| | | <el-descriptions-item label="éä»¶"> |
| | | <ImageUpload v-if="attachments && attachments.length > 0" v-model:file-list="attachments" :disabled="true" /> |
| | | <span v-else style="color: #999;">æ éä»¶</span> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <el-form ref="formRef" :model="form" label-width="100px" style="margin-top: 20px;"> |
| | | <el-form-item label="å®¡æ¹æè§" prop="approveReason" v-if="operationType === 'approval'"> |
| | | <el-input v-model="form.approveReason" type="textarea" placeholder="请è¾å
¥å®¡æ¹æè§" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <template #footer |
| | | v-if="operationType === 'approval'"> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" |
| | | :loading="submitLoading" |
| | | @click="submitForm(2)">ä¸éè¿</el-button> |
| | | <el-button type="primary" |
| | | :loading="submitLoading" |
| | | @click="submitForm(1)">éè¿</el-button> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { |
| | | computed, |
| | | getCurrentInstance, |
| | | nextTick, |
| | | reactive, |
| | | ref, |
| | | toRefs, |
| | | } from "vue"; |
| | | import { |
| | | approveProcessDetails, |
| | | getDept, |
| | | updateApproveNode, |
| | | } from "@/api/collaborativeApproval/approvalProcess.js"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import { |
| | | WarningFilled, |
| | | Edit, |
| | | Check, |
| | | MoreFilled, |
| | | } from "@element-plus/icons-vue"; |
| | | import { getQuotationList } from "@/api/salesManagement/salesQuotation.js"; |
| | | import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js"; |
| | | import { getDeliveryDetailByShippingNo } from "@/api/salesManagement/deliveryLedger.js"; |
| | | import ImagePreview from "@/components/AttachmentPreview/image/index.vue"; |
| | | import { formatCurrency } from "@/utils/numberFormat"; |
| | | import ImageUpload from "@/components/AttachmentUpload/image/index.vue"; |
| | | import { attachmentList } from "@/api/basicData/storageAttachment.js"; |
| | | |
| | | const emit = defineEmits(["close"]); |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const props = defineProps({ |
| | | approveType: { |
| | | type: [Number, String], |
| | | default: 0, |
| | | }, |
| | | }); |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref(""); |
| | | const activities = ref([]); |
| | | const attachments = ref([]); |
| | | const formRef = ref(null); |
| | | const userStore = useUserStore(); |
| | | const productOptions = ref([]); |
| | | const quotationLoading = ref(false); |
| | | const currentQuotation = ref({}); |
| | | const purchaseLoading = ref(false); |
| | | const currentPurchase = ref({}); |
| | | const deliveryLoading = ref(false); |
| | | const currentDelivery = ref({}); |
| | | const deliveryProductList = ref([]); |
| | | const submitLoading = ref(false); |
| | | const isQuotationApproval = computed(() => Number(props.approveType) === 6); |
| | | const isPurchaseApproval = computed(() => Number(props.approveType) === 5); |
| | | const isDeliveryApproval = computed(() => Number(props.approveType) === 7); |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | approveId: "", |
| | | approveDeptId: "", |
| | | approveReason: "", |
| | | checkResult: "", |
| | | }, |
| | | }); |
| | | const { form } = toRefs(data); |
| | | |
| | | // èç¹æ é¢ |
| | | const getNodeTitle = (index, len) => { |
| | | if (index === len - 1) return "ç»æ"; |
| | | return "审æ¹"; |
| | | }; |
| | | |
| | | // è·åå½åæ¿æ´»æ¥éª¤ |
| | | const getActiveStep = () => { |
| | | // å¦æææ isShen é½ä¸º falseï¼è¿åæåä¸ä¸ªæ¥éª¤ï¼å
¨é¨å®æï¼ |
| | | const hasActive = activities.value.some(a => a.isShen === true); |
| | | if (!hasActive) return activities.value.length; |
| | | // å½åèç¹ç´¢å¼ |
| | | return activities.value.findIndex(a => a.isShen == true); |
| | | }; |
| | | // æ¥éª¤icon |
| | | const getNodeIcon = (activity, index) => { |
| | | if (activity.approveNodeStatus === 2) return "el-icon-warning"; // ä¸éè¿ |
| | | if (activity.isShen) return "Edit"; |
| | | return ""; |
| | | }; |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | currentQuotation.value = {}; |
| | | currentPurchase.value = {}; |
| | | form.value = { ...row }; |
| | | |
| | | // è·åéä»¶ |
| | | attachments.value = []; |
| | | attachmentList({ recordId: row.id, recordType: 'approve_process' }).then(res => { |
| | | attachments.value = res.data || []; |
| | | }); |
| | | // ç«å³æ¸
é¤è¡¨åéªè¯ç¶æï¼å ä¸ºåæ®µæ¯disabledçï¼ä¸éè¦éªè¯ï¼ |
| | | nextTick(() => { |
| | | if (formRef.value) { |
| | | formRef.value.clearValidate(); |
| | | } |
| | | }); |
| | | // ç¡®ä¿é项å è½½å®æååå¹é
å¼ç±»å |
| | | getProductOptions().then(() => { |
| | | // ç¡®ä¿å¼ç±»åå¹é
ï¼å¦æé项已å è½½ï¼ |
| | | if (productOptions.value.length > 0 && form.value.approveDeptId) { |
| | | const matchedOption = productOptions.value.find( |
| | | opt => |
| | | opt.deptId == form.value.approveDeptId || |
| | | String(opt.deptId) === String(form.value.approveDeptId) |
| | | ); |
| | | if (matchedOption) { |
| | | form.value.approveDeptId = matchedOption.deptId; |
| | | } |
| | | } |
| | | // 忬¡æ¸
é¤éªè¯ï¼ç¡®ä¿é项å è½½åå¼å¹é
æ£ç¡® |
| | | nextTick(() => { |
| | | if (formRef.value) { |
| | | formRef.value.clearValidate(); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | // æ¥ä»·å®¡æ¹ï¼ç¨å®¡æ¹äºç±å段æ¿è½½ç"æ¥ä»·åå·"廿¥æ¥ä»·å表 |
| | | if (isQuotationApproval.value) { |
| | | const quotationNo = row?.approveReason; |
| | | if (quotationNo) { |
| | | quotationLoading.value = true; |
| | | getQuotationList({ quotationNo }) |
| | | .then(res => { |
| | | const records = res?.data?.records || []; |
| | | currentQuotation.value = records[0] || {}; |
| | | }) |
| | | .finally(() => { |
| | | quotationLoading.value = false; |
| | | }); |
| | | } |
| | | } |
| | | |
| | | // éè´å®¡æ¹ï¼ç¨å®¡æ¹äºç±å段æ¿è½½ç"éè´ååå·"廿¥éè´è¯¦æ
|
| | | if (isPurchaseApproval.value) { |
| | | const purchaseContractNumber = row?.approveReason; |
| | | if (purchaseContractNumber) { |
| | | purchaseLoading.value = true; |
| | | getPurchaseByCode({ purchaseContractNumber }) |
| | | .then(res => { |
| | | currentPurchase.value = res; |
| | | }) |
| | | .catch(err => { |
| | | console.error("æ¥è¯¢éè´è¯¦æ
失败:", err); |
| | | proxy.$modal.msgError("æ¥è¯¢éè´è¯¦æ
失败"); |
| | | }) |
| | | .finally(() => { |
| | | purchaseLoading.value = false; |
| | | }); |
| | | } |
| | | } |
| | | // å货审æ¹ï¼ç¨å®¡æ¹äºç±å段æ¿è½½ç"åè´§åå·"廿¥å货详æ
|
| | | if (isDeliveryApproval.value) { |
| | | const deliveryNo = row?.approveReason; |
| | | if (deliveryNo) { |
| | | deliveryLoading.value = true; |
| | | currentDelivery.value = {}; |
| | | deliveryProductList.value = []; |
| | | getDeliveryDetailByShippingNo({ shippingNo: deliveryNo }) |
| | | .then(res => { |
| | | const detailData = res?.data || res || {}; |
| | | currentDelivery.value = detailData; |
| | | deliveryProductList.value = |
| | | detailData.shippingProductDetailDtoList || []; |
| | | }) |
| | | .catch(err => { |
| | | console.error("æ¥è¯¢å货详æ
失败:", err); |
| | | proxy.$modal.msgError("æ¥è¯¢å货详æ
失败"); |
| | | }) |
| | | .finally(() => { |
| | | deliveryLoading.value = false; |
| | | }); |
| | | } |
| | | } |
| | | |
| | | approveProcessDetails(row.approveId).then(res => { |
| | | activities.value = res.data; |
| | | // å¢å isApprovalåæ®µ |
| | | activities.value.forEach(item => { |
| | | if (item.url && item.url.includes("word")) { |
| | | item.urlTem = item.url.replaceAll("word", "img"); |
| | | } else { |
| | | item.urlTem = item.url; |
| | | } |
| | | if (item.approveNodeStatus === 2) { |
| | | item.isApproval = "已驳å"; |
| | | } else if (item.approveNodeStatus === 1) { |
| | | item.isApproval = "å·²åæ"; |
| | | } else { |
| | | item.isApproval = "æªå®¡æ¹"; |
| | | } |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const getDeliveryProductInfoList = () => { |
| | | const row = currentDelivery.value; |
| | | if (!row) return []; |
| | | const normalizeBatchNoList = value => { |
| | | if (Array.isArray(value)) return value; |
| | | if (typeof value === "string" && value.includes(",")) { |
| | | return value |
| | | .split(",") |
| | | .map(item => item.trim()) |
| | | .filter(Boolean); |
| | | } |
| | | return value ? [value] : []; |
| | | }; |
| | | const detailList = deliveryProductList.value.length |
| | | ? deliveryProductList.value |
| | | : [ |
| | | row.batchNoDetailList, |
| | | row.batchNoList, |
| | | row.shippingBatchList, |
| | | row.shippingInfoDetailList, |
| | | row.detailList, |
| | | row.batchDetailList, |
| | | ].find(value => Array.isArray(value) && value.length); |
| | | const batchNoList = normalizeBatchNoList(row.batchNo); |
| | | const toTableRow = (item = {}) => ({ |
| | | batchNo: |
| | | typeof item === "string" || typeof item === "number" |
| | | ? item |
| | | : item.batchNo ?? item.batchNumber ?? row.batchNo ?? "--", |
| | | productName: item.productName ?? row.productName ?? "--", |
| | | specificationModel: |
| | | item.specificationModel ?? item.model ?? row.specificationModel ?? "--", |
| | | deliveryQuantity: |
| | | item.deliveryQuantity ?? |
| | | item.quantity ?? |
| | | item.shippingQuantity ?? |
| | | row.deliveryQuantity ?? |
| | | row.quantity ?? |
| | | "--", |
| | | }); |
| | | if (detailList?.length) { |
| | | return detailList.map(toTableRow); |
| | | } |
| | | if (batchNoList.length) { |
| | | return batchNoList.map(batchNo => toTableRow({ batchNo })); |
| | | } |
| | | return [toTableRow()]; |
| | | }; |
| | | const getApprovalStatusText = status => { |
| | | const statusMap = { |
| | | 0: "å¾
å®¡æ ¸", |
| | | 1: "å®¡æ ¸éè¿", |
| | | 2: "å®¡æ ¸æç»", |
| | | 3: "å®¡æ ¸ä¸", |
| | | }; |
| | | return statusMap[status] || "å¾
å®¡æ ¸"; |
| | | }; |
| | | |
| | | const getProductOptions = () => { |
| | | return getDept().then(res => { |
| | | productOptions.value = res.data; |
| | | }); |
| | | }; |
| | | // æäº¤å®¡æ¹ |
| | | const submitForm = status => { |
| | | const filteredActivities = activities.value.filter( |
| | | activity => activity.isShen |
| | | ); |
| | | if (!filteredActivities || filteredActivities.length === 0) { |
| | | proxy.$modal.msgError("æªæ¾å°å¾
审æ¹çèç¹"); |
| | | return; |
| | | } |
| | | const currentActivity = filteredActivities[0]; |
| | | if (!currentActivity) { |
| | | proxy.$modal.msgError("æªæ¾å°å¾
审æ¹çèç¹"); |
| | | return; |
| | | } |
| | | currentActivity.approveNodeStatus = status; |
| | | // 夿æ¯å¦ä¸ºæå䏿¥ |
| | | const isLast = |
| | | activities.value.findIndex(a => a.isShen) === activities.value.length - 1; |
| | | submitLoading.value = true; |
| | | updateApproveNode({ ...currentActivity, isLast }) |
| | | .then(() => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | }) |
| | | .finally(() => { |
| | | submitLoading.value = false; |
| | | }); |
| | | }; |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | | dialogFormVisible.value = false; |
| | | quotationLoading.value = false; |
| | | currentQuotation.value = {}; |
| | | purchaseLoading.value = false; |
| | | currentPurchase.value = {}; |
| | | submitLoading.value = false; |
| | | emit("close"); |
| | | }; |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .node-user { |
| | | margin: 10px 0; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .node-status { |
| | | color: #1890ff; |
| | | margin-left: 8px; |
| | | font-size: 14px; |
| | | } |
| | | .node-reason { |
| | | font-size: 15px; |
| | | color: #333; |
| | | margin: 10px 0; |
| | | } |
| | | .user-avatar { |
| | | cursor: pointer; |
| | | width: 30px; |
| | | height: 30px; |
| | | border-radius: 50px; |
| | | } |
| | | .signImg { |
| | | cursor: pointer; |
| | | width: 200px; |
| | | height: 60px; |
| | | } |
| | | .approval-detail :deep(.el-upload) { |
| | | display: none !important; |
| | | } |
| | | .approval-detail :deep(.upload-tip) { |
| | | display: none !important; |
| | | } |
| | | </style> |
| | |
| | | </el-card> |
| | | |
| | | <!-- å¼¹çªç»ä»¶ --> |
| | | <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="currentApproveType"></info-form-dia> |
| | | <approval-dia ref="approvalDia" @close="handleQuery" :approveType="currentApproveType"></approval-dia> |
| | | <info-form-dia ref="infoFormDia" @close="handleDialogClose" :approveType="currentApproveType"></info-form-dia> |
| | | <approval-dia ref="approvalDia" @close="handleDialogClose" :approveType="currentApproveType"></approval-dia> |
| | | <view-detail-dia ref="viewDetailDia" @close="handleDialogClose" :approveType="currentApproveType"></view-detail-dia> |
| | | <FileList v-if="fileDialogVisible" |
| | | v-model:visible="fileDialogVisible" |
| | | record-type="approve_process" |
| | |
| | | import { useRoute } from 'vue-router'; |
| | | import InfoFormDia from "@/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue"; |
| | | import ApprovalDia from "@/views/collaborativeApproval/approvalProcess/components/approvalDia.vue"; |
| | | import ViewDetailDia from "@/views/collaborativeApproval/approvalProcess/components/viewDetailDia.vue"; |
| | | import {approveProcessDelete, approveProcessListPage} from "@/api/collaborativeApproval/approvalProcess.js"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue")); |
| | |
| | | }); |
| | | const infoFormDia = ref() |
| | | const approvalDia = ref() |
| | | const viewDetailDia = ref() |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | | // æ¥è¯¢å表 |
| | | /** æç´¢æé®æä½ */ |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | const handleDialogClose = () => { |
| | | getList(); |
| | | }; |
| | | |
| | |
| | | page.total = res.data.total; |
| | | // æ´æ°å½åç±»åæ°é |
| | | typeCounts.value[activeTab.value] = res.data.total; |
| | | if (res.data.records.length === 0 && page.current > 1) { |
| | | page.current = Math.max(1, Math.ceil(res.data.total / page.size)); |
| | | getList(); |
| | | } |
| | | }).catch(err => { |
| | | tableLoading.value = false; |
| | | }) |
| | |
| | | // æå¼æ°å¢æ£éªå¼¹æ¡ |
| | | const openApprovalDia = (type, row) => { |
| | | nextTick(() => { |
| | | if (type === 'view') { |
| | | viewDetailDia.value?.openDialog(type, row) |
| | | } else { |
| | | approvalDia.value?.openDialog(type, row) |
| | | } |
| | | }) |
| | | }; |
| | | |
| | |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="设å¤åç§°">{{ detailData.deviceName }}</el-descriptions-item> |
| | | <el-descriptions-item label="è§æ ¼åå·">{{ detailData.deviceModel }}</el-descriptions-item> |
| | | <el-descriptions-item label="设å¤åç">{{ detailData.deviceBrand }}</el-descriptions-item> |
| | | <!-- <el-descriptions-item label="设å¤åç">{{ detailData.deviceBrand }}</el-descriptions-item> --> |
| | | <el-descriptions-item label="设å¤ç±»å">{{ detailData.type }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä¾åºå">{{ detailData.supplierName }}</el-descriptions-item> |
| | | <el-descriptions-item label="åæ¾ä½ç½®">{{ detailData.storageLocation }}</el-descriptions-item> |
| | | <!-- <el-descriptions-item label="åæ¾ä½ç½®">{{ detailData.storageLocation }}</el-descriptions-item> --> |
| | | <el-descriptions-item label="åä½">{{ detailData.unit }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ°é">{{ detailData.number }}</el-descriptions-item> |
| | | <el-descriptions-item label="å¯ç¨ææ§">{{ detailData.isDepr === 1 ? 'æ¯' : 'å¦' }}</el-descriptions-item> |
| | |
| | | label: "è§æ ¼åå·", |
| | | prop: "deviceModel", |
| | | }, |
| | | { |
| | | label: "设å¤åç", |
| | | prop: "deviceBrand", |
| | | }, |
| | | // { |
| | | // label: "设å¤åç", |
| | | // prop: "deviceBrand", |
| | | // }, |
| | | { |
| | | label: "设å¤ç±»å", |
| | | prop: "type", |
| | |
| | | label: "ä¾åºå", |
| | | prop: "supplierName", |
| | | }, |
| | | { |
| | | label: "åæ¾ä½ç½®", |
| | | prop: "storageLocation", |
| | | }, |
| | | // { |
| | | // label: "åæ¾ä½ç½®", |
| | | // prop: "storageLocation", |
| | | // }, |
| | | { |
| | | label: "æ°é", |
| | | prop: "number", |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog |
| | | v-model="visible" |
| | | title="è®¾å¤æ¥ä¿®è¯¦æ
" |
| | | width="800px" |
| | | destroy-on-close |
| | | :close-on-click-modal="false" |
| | | @close="handleClose" |
| | | > |
| | | <el-descriptions border :column="2" class="repair-detail"> |
| | | <el-descriptions-item label="设å¤åç§°">{{ form.deviceName }}</el-descriptions-item> |
| | | <el-descriptions-item label="è§æ ¼åå·">{{ form.deviceModel }}</el-descriptions-item> |
| | | |
| | | <el-descriptions-item label="æ¥ä¿®æ¥æ"> |
| | | {{ form.repairTime ? dayjs(form.repairTime).format("YYYY-MM-DD") : "" }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="æ¥ä¿®äºº">{{ form.repairName }}</el-descriptions-item> |
| | | |
| | | <el-descriptions-item label="æ¥ä¿®é¡¹ç®">{{ form.machineryCategory }}</el-descriptions-item> |
| | | <el-descriptions-item label="维修人">{{ form.maintenanceName }}</el-descriptions-item> |
| | | |
| | | <el-descriptions-item label="æ¥ä¿®ç¶æ"> |
| | | <el-tag v-if="form.status === 2" type="danger">失败</el-tag> |
| | | <el-tag v-else-if="form.status === 1" type="success">å·²éªæ¶</el-tag> |
| | | <el-tag v-else-if="form.status === 3" type="info">å¾
éªæ¶</el-tag> |
| | | <el-tag v-else-if="form.status === 0" type="warning">å¾
ç»´ä¿®</el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="æ
éç°è±¡">{{ form.remark }}</el-descriptions-item> |
| | | |
| | | <!-- éªæ¶ä¿¡æ¯ --> |
| | | <template v-if="form.status === 1 || form.status === 3"> |
| | | <el-descriptions-item label="éªæ¶äºº">{{ form.acceptanceName }}</el-descriptions-item> |
| | | <el-descriptions-item label="éªæ¶æ¶é´">{{ form.acceptanceTime }}</el-descriptions-item> |
| | | <el-descriptions-item label="éªæ¶å¤æ³¨" :span="2">{{ form.acceptanceRemark }}</el-descriptions-item> |
| | | </template> |
| | | |
| | | <!-- éä»¶ --> |
| | | <el-descriptions-item label="éä»¶" :span="2"> |
| | | <ImageUpload |
| | | v-if="form.storageBlobDTOs && form.storageBlobDTOs.length > 0" |
| | | v-model:file-list="form.storageBlobDTOs" |
| | | :disabled="true" |
| | | /> |
| | | <span v-else style="color: #999;">æ éä»¶</span> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="handleClose">å
³é</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, nextTick } from "vue"; |
| | | import dayjs from "dayjs"; |
| | | import { getRepairById } from "@/api/equipmentManagement/repair"; |
| | | import ImageUpload from "@/components/AttachmentUpload/image/index.vue"; |
| | | |
| | | const visible = ref(false); |
| | | const form = ref({}); |
| | | |
| | | const openView = async (id) => { |
| | | visible.value = true; |
| | | await nextTick(); |
| | | try { |
| | | const { data } = await getRepairById(id); |
| | | // ç¡®ä¿éä»¶åæ®µåç§°å¹é
ç»ä»¶éè¦ |
| | | data.storageBlobDTOs = data.storageBlobVOs || []; |
| | | form.value = data || {}; |
| | | } catch (error) { |
| | | console.error("Failed to load repair details:", error); |
| | | } |
| | | }; |
| | | |
| | | const handleClose = () => { |
| | | visible.value = false; |
| | | form.value = {}; |
| | | }; |
| | | |
| | | defineExpose({ |
| | | openView, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .repair-detail :deep(.el-descriptions__label) { |
| | | width: 120px; |
| | | justify-content: flex-end; |
| | | } |
| | | .repair-detail :deep(.el-upload) { |
| | | display: none !important; |
| | | } |
| | | </style> |
| | |
| | | </div> |
| | | <RepairModal ref="repairModalRef" |
| | | @ok="getTableData" /> |
| | | <RepairDetailModal ref="repairDetailModalRef" /> |
| | | <MaintainModal ref="maintainModalRef" |
| | | @ok="getTableData" /> |
| | | <AcceptanceModal ref="acceptanceModalRef" |
| | |
| | | import { usePaginationApi } from "@/hooks/usePaginationApi"; |
| | | import { getRepairPage, delRepair } from "@/api/equipmentManagement/repair"; |
| | | import RepairModal from "./Modal/RepairModal.vue"; |
| | | import RepairDetailModal from "./Modal/RepairDetailModal.vue"; |
| | | import { ElMessageBox, ElMessage } from "element-plus"; |
| | | import dayjs from "dayjs"; |
| | | import MaintainModal from "./Modal/MaintainModal.vue"; |
| | |
| | | |
| | | // æ¨¡ææ¡å®ä¾ |
| | | const repairModalRef = ref(); |
| | | const repairDetailModalRef = ref(); |
| | | const maintainModalRef = ref(); |
| | | const acceptanceModalRef = ref(); |
| | | |
| | |
| | | return multipleList.value.some(item => item.status === 1); |
| | | }); |
| | | |
| | | // æ°å¢æ¥ä¿® |
| | | // æ°å¢è®¾å¤æ¥ä¿® |
| | | const addRepair = () => { |
| | | repairModalRef.value.openAdd(); |
| | | }; |
| | | |
| | | // 详æ
æ¥ç |
| | | // æ¥ç详æ
|
| | | const viewRepair = id => { |
| | | repairModalRef.value.openView(id); |
| | | repairDetailModalRef.value.openView(id); |
| | | }; |
| | | |
| | | // ç¼è¾æ¥ä¿® |
| | |
| | | <el-form-item label="åºåæ°é" |
| | | prop="qualitity"> |
| | | <el-input-number v-model="formState.qualitity" |
| | | :step="1" |
| | | :min="1" |
| | | :step="0.00000001" |
| | | :min="0" |
| | | :precision="8" |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¹å·" |
| | |
| | | label="åºåé¢è¦æ°é" |
| | | prop="warnNum"> |
| | | <el-input-number v-model="formState.warnNum" |
| | | :step="1" |
| | | :step="0.00000001" |
| | | :min="0" |
| | | :precision="8" |
| | | :max="formState.qualitity" |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | |
| | | productName: "", |
| | | productModelName: "", |
| | | unit: "", |
| | | type: undefined, |
| | | type: "qualified", |
| | | qualitity: 0, |
| | | batchNo: null, |
| | | warnNum: 0, |
| | |
| | | productName: "", |
| | | productModelName: "", |
| | | unit: "", |
| | | type: undefined, |
| | | type: "qualified", |
| | | qualitity: 0, |
| | | batchNo: null, |
| | | warnNum: 0, |
| | |
| | |
|
| | | <el-table ref="logininforRef" v-loading="loading" :data="logininforList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange">
|
| | | <el-table-column type="selection" width="55" align="center" />
|
| | | <el-table-column label="访é®ç¼å·" align="center" prop="infoId" />
|
| | | <el-table-column label="访é®ç¼å·" align="center">
|
| | | <template #default="scope">
|
| | | <span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
|
| | | </template>
|
| | | </el-table-column>
|
| | | <el-table-column label="ç¨æ·åç§°" align="center" prop="userName" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']" />
|
| | | <el-table-column label="å°å" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
|
| | | <el-table-column label="ç»å½å°ç¹" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
|
| | |
| | |
|
| | | <el-table ref="operlogRef" v-loading="loading" :data="operlogList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange">
|
| | | <el-table-column type="selection" width="50" align="center" />
|
| | | <el-table-column label="æ¥å¿ç¼å·" align="center" prop="operId" />
|
| | | <el-table-column label="æ¥å¿ç¼å·" align="center">
|
| | | <template #default="scope">
|
| | | <span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
|
| | | </template>
|
| | | </el-table-column>
|
| | | <el-table-column label="ç³»ç»æ¨¡å" align="center" prop="title" :show-overflow-tooltip="true" />
|
| | | <el-table-column label="æä½ç±»å" align="center" prop="businessType">
|
| | | <template #default="scope">
|
| | |
| | | :total="total"
|
| | | v-model:page="queryParams.pageNum"
|
| | | v-model:limit="queryParams.pageSize"
|
| | | :page-sizes="[10, 20, 30, 50, 100, 200]"
|
| | | @pagination="getList"
|
| | | />
|
| | |
|
| | |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | // å°æªå
¥åºéé¢å¹¶å
¥å·²å
¥åºéé¢ï¼æªå
¥åºéé¢ç½®0 |
| | | const mergeUnshippedAmount = row => { |
| | | if (row && row.unshippedAmount) { |
| | | row.shippedAmount = (Number(row.shippedAmount) || 0) + (Number(row.unshippedAmount) || 0); |
| | | row.unshippedAmount = 0; |
| | | } |
| | | return row; |
| | | }; |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | paymentLedgerList({ |
| | |
| | | }).then(res => { |
| | | let result = res.data; |
| | | tableLoading.value = false; |
| | | tableData.value = result.records || []; |
| | | tableData.value = (result.records || []).map(mergeUnshippedAmount); |
| | | total.value = result.total || 0; |
| | | if (tableData.value.length > 0) { |
| | | currentSupplierId.value = tableData.value[0].supplierId; |
| | |
| | | tableLoadingSon.value = false; |
| | | let result = res.data; |
| | | if (Array.isArray(result)) { |
| | | tableDataSon.value = result; |
| | | tableDataSon.value = result.map(mergeUnshippedAmount); |
| | | sonPage.total = result.length; |
| | | handlePagination({ page: sonPage.current, limit: sonPage.size }); |
| | | } else { |
| | | originalTableDataSon.value = result.records || []; |
| | | originalTableDataSon.value = (result.records || []).map(mergeUnshippedAmount); |
| | | sonPage.total = result.total || 0; |
| | | } |
| | | }) |
| | |
| | | <el-table-column label="å
¥åºç¶æ" prop="stockInStatus" width="100" show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | <el-tag :type="getStockInStatusType(scope.row.stockInStatus)" size="small"> |
| | | {{ scope.row.stockInStatus || '--' }} |
| | | {{ formatStockInStatus(scope.row.stockInStatus) || '--' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¾åºååç§°ï¼" prop="supplierId"> |
| | | <el-select v-model="form.supplierId" placeholder="è¯·éæ©" filterable clearable> |
| | | <el-option v-for="item in supplierList" :key="item.id" :label="item.supplierName" :value="item.id">{{ item.supplierType ? item.supplierName + '---' + item.supplierType : item.supplierName }}</el-option> |
| | | <el-option v-for="item in supplierList" :key="item.id" :label="item.supplierName" :value="item.id">{{ item.supplierName }}</el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | return typeMap[status] || ""; |
| | | }; |
| | | |
| | | // å
¥åºç¶ææ¾ç¤ºï¼å
¥åºä¸ä¹å±ç¤ºä¸ºå®å
¨å
¥åº |
| | | const formatStockInStatus = status => { |
| | | if (status === "å
¥åºä¸") return "å®å
¨å
¥åº"; |
| | | return status; |
| | | }; |
| | | |
| | | // è·åå
¥åºç¶ææ ç¾ç±»å |
| | | const getStockInStatusType = status => { |
| | | const typeMap = { |
| | | "å¾
å
¥åº": "info", // å¾
å
¥åº - ç°è² |
| | | "å
¥åºä¸": "warning", // å
¥åºä¸ - æ©è² |
| | | "å
¥åºä¸": "success", // å
¥åºä¸ - ç»¿è² |
| | | "å®å
¨å
¥åº": "success", // å®å
¨å
¥åº - ç»¿è² |
| | | }; |
| | | return typeMap[status] || ""; |
| | |
| | | <div class="checkbox-group"> |
| | | <el-checkbox v-model="formData.summaryMaterial">ç©æ</el-checkbox> |
| | | <el-checkbox v-model="formData.summaryAuxAttributes">è¾
å©å±æ§</el-checkbox> |
| | | <el-checkbox v-model="formData.summaryDemandDate">éæ±æ¥æ</el-checkbox> |
| | | <el-checkbox v-model="formData.summaryDemandDate">äº¤è´§æ¥æ</el-checkbox> |
| | | </div> |
| | | </div> |
| | | |
| | |
| | | :step="1" |
| | | controls-position="right" |
| | | style="width: 100%" |
| | | @change="handleUnitQuantityChange" |
| | | @change="handleUnitQuantityChange(row)" |
| | | :disabled="!bomDataValue.isEdit || bomDataValue.dataList.some(item => (item).tempId === row.tempId)" /> |
| | | </el-form-item> |
| | | </template> |
| | |
| | | }); |
| | | }; |
| | | |
| | | const toQuantityNumber = value => { |
| | | const numberValue = Number(value); |
| | | if (!Number.isFinite(numberValue)) { |
| | | return 0; |
| | | } |
| | | return Number(numberValue.toFixed(4)); |
| | | }; |
| | | |
| | | const syncDemandedQuantityTree = (items, parentDemandedQuantity = null) => { |
| | | items.forEach(item => { |
| | | if (parentDemandedQuantity !== null) { |
| | | item.demandedQuantity = toQuantityNumber( |
| | | parentDemandedQuantity * toQuantityNumber(item.unitQuantity) |
| | | ); |
| | | } |
| | | |
| | | if (Array.isArray(item.children) && item.children.length > 0) { |
| | | syncDemandedQuantityTree( |
| | | item.children, |
| | | toQuantityNumber(item.demandedQuantity) |
| | | ); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const recalculateDemandedQuantities = () => { |
| | | if (pageType.value !== "order") { |
| | | return; |
| | | } |
| | | |
| | | const rootDemandedQuantity = routeInfo.value.quantity; |
| | | if ( |
| | | rootDemandedQuantity === undefined || |
| | | rootDemandedQuantity === null || |
| | | rootDemandedQuantity === "" |
| | | ) { |
| | | syncDemandedQuantityTree(bomDataValue.value.dataList); |
| | | return; |
| | | } |
| | | |
| | | syncDemandedQuantityTree( |
| | | bomDataValue.value.dataList, |
| | | toQuantityNumber(rootDemandedQuantity) |
| | | ); |
| | | }; |
| | | |
| | | const processChange = value => { |
| | | processOptions.value.forEach(item => { |
| | | if (item.id == value) { |
| | |
| | | } |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | const toQuantityNumber = value => { |
| | | const numberValue = Number(value); |
| | | if (!Number.isFinite(numberValue)) { |
| | | return 0; |
| | | } |
| | | return Number(numberValue.toFixed(4)); |
| | | }; |
| | | |
| | | const handleUnitQuantityChange = row => { |
| | | if (pageType.value !== "order") { |
| | | return; |
| | | } |
| | | |
| | | row.demandedQuantity = toQuantityNumber( |
| | | toQuantityNumber(row.unitQuantity) * toQuantityNumber(routeInfo.value.quantity) |
| | | ); |
| | | }; |
| | | |
| | | const handleBomProcessChange = (row, value) => { |
| | |
| | | ); |
| | | bomDataValue.value.dataList = data || []; |
| | | normalizeTreeData(bomDataValue.value.dataList); |
| | | recalculateDemandedQuantities(); |
| | | } catch (err) { |
| | | console.error("è·åBOMæ°æ®å¤±è´¥ï¼", err); |
| | | } |
| | |
| | | }); |
| | | }; |
| | | |
| | | const handleUnitQuantityChange = () => { |
| | | recalculateDemandedQuantities(); |
| | | }; |
| | | |
| | | const addchildItem = (item, tempId) => { |
| | | if (item.tempId === tempId) { |
| | | if (!item.children) { |
| | |
| | | unit: "", |
| | | tempId: new Date().getTime(), |
| | | }); |
| | | recalculateDemandedQuantities(); |
| | | return true; |
| | | } |
| | | if (item.children && item.children.length > 0) { |
| | |
| | | children: [], |
| | | tempId: new Date().getTime(), |
| | | }); |
| | | recalculateDemandedQuantities(); |
| | | return; |
| | | } |
| | | addchildItem(item, tempId); |
| | |
| | | console.log(bomDataValue.value.dataList, "bomDataValue.value.dataList"); |
| | | |
| | | normalizeTreeData(bomDataValue.value.dataList); |
| | | recalculateDemandedQuantities(); |
| | | |
| | | const valid = validateAllBom(); |
| | | if (valid) { |
| | |
| | | border |
| | | :preserve-expanded-content="false" |
| | | :default-expand-all="true" |
| | | style="width: 100%"> |
| | | style="width: 100%" |
| | | @expand-change="handleOuterExpandChange"> |
| | | <el-table-column type="expand"> |
| | | <template #default="props"> |
| | | <el-form ref="form" |
| | | :model="dataValue"> |
| | | <el-table :data="dataValue.dataList" |
| | | <div v-if="dataValue.isEdit" |
| | | class="drag-tip"> |
| | | <el-icon><Rank /></el-icon> |
| | | <span>æå¨ææè°æ´ç»æ:åå±çº§ç´æ¥æå¨æåº;æå°æè¡æ£ä¸æ¹æä¸ºå
¶å级;æå°å表æåæä¸ºä¸çº§</span> |
| | | </div> |
| | | <el-table ref="tableRef" |
| | | :data="dataValue.dataList" |
| | | row-key="tempId" |
| | | default-expand-all |
| | | :tree-props="{children: 'children', hasChildren: 'hasChildren'}" |
| | | @expand-change="handleTreeExpandChange" |
| | | style="width: 100%"> |
| | | <el-table-column v-if="dataValue.isEdit" |
| | | width="44" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | <div class="drag-handle" |
| | | title="æå¨è°æ´é¡ºåº:æå°æè¡æ£ä¸æ¹æä¸ºå
¶å级,æå°å表æåæä¸ºä¸çº§"> |
| | | <el-icon><Rank /></el-icon> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="productName" |
| | | label="产å" /> |
| | | <el-table-column prop="model" |
| | |
| | | <el-form-item v-if="dataValue.isEdit" |
| | | :rules="dataValue.dataList.some(item => (item as any).tempId === row.tempId) ? [] : [{ required: true, message: 'è¯·éæ©æ¶èå·¥åº', trigger: 'change' }]" |
| | | style="margin: 0"> |
| | | <el-select v-model="row.processId" |
| | | placeholder="è¯·éæ©" |
| | | filterable |
| | | <el-input v-model="row.processName" |
| | | readonly |
| | | placeholder="è¯·éæ©æ¶èå·¥åº" |
| | | clearable |
| | | style="width: 100%" |
| | | @change="value => handleProcessChange(row, value)" |
| | | :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)"> |
| | | <el-option v-for="item in dataValue.processOptions" |
| | | :key="item.id" |
| | | :label="item.name" |
| | | :value="item.id" /> |
| | | </el-select> |
| | | :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" |
| | | @click="openProcessDialog(row, $event)" |
| | | @clear="clearProcess(row)"> |
| | | <template #append> |
| | | <el-button icon="Search" |
| | | :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" |
| | | @click="openProcessDialog(row)" /> |
| | | </template> |
| | | </el-input> |
| | | </el-form-item> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | v-model:model-value="dataValue.showProductDialog" |
| | | :single="true" |
| | | @confirm="handleProduct" /> |
| | | <process-select-dialog v-if="dataValue.showProcessDialog" |
| | | v-model:model-value="dataValue.showProcessDialog" |
| | | @confirm="handleProcessConfirm" /> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | computed, |
| | | defineAsyncComponent, |
| | | defineComponent, |
| | | nextTick, |
| | | onBeforeUnmount, |
| | | onMounted, |
| | | reactive, |
| | | ref, |
| | | watch, |
| | | } from "vue"; |
| | | import { |
| | | queryList, |
| | | addBomDetail, |
| | | } from "@/api/productionManagement/productStructure.js"; |
| | | import { listProcessBom } from "@/api/productionManagement/productionOrder.js"; |
| | | import { list } from "@/api/productionManagement/productionProcess"; |
| | | import { listAll } from "@/api/productionManagement/productionProcess"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { useRoute, useRouter } from "vue-router"; |
| | | import Sortable from "sortablejs"; |
| | | import { Rank } from "@element-plus/icons-vue"; |
| | | |
| | | defineComponent({ |
| | | name: "StructureEdit", |
| | |
| | | const ProductSelectDialog = defineAsyncComponent( |
| | | () => import("@/views/basicData/product/ProductSelectDialog.vue") |
| | | ); |
| | | const ProcessSelectDialog = defineAsyncComponent( |
| | | () => |
| | | import( |
| | | "@/views/productionManagement/productionProcess/ProcessSelectDialog.vue" |
| | | ) |
| | | ); |
| | | const emit = defineEmits(["update:router"]); |
| | | const form = ref(); |
| | | const tableRef = ref(); |
| | | let tableSortable = null; |
| | | |
| | | const route = useRoute(); |
| | | const router = useRouter(); |
| | |
| | | productOptions: [], |
| | | processOptions: [], |
| | | showProductDialog: false, |
| | | showProcessDialog: false, |
| | | currentRowIndex: null, |
| | | currentRowName: null, |
| | | currentProcessRow: null, |
| | | loading: false, |
| | | isEdit: false, |
| | | }); |
| | |
| | | syncDemandedQuantityTree(dataValue.dataList); |
| | | }; |
| | | |
| | | // ===== ææ½æåº ===== |
| | | // æå½å DOM è¡é¡ºåºå±å¼ææå¹³å表(ç¼è¾æ¨¡å¼ä¸å¼ºå¶å
¨é¨å±å¼,ä¿è¯è¡é¡ºåºä¸æ°æ®ä¸è´) |
| | | const buildFlatTree = () => { |
| | | const flat = []; |
| | | const walk = (items, depth, parentNode) => { |
| | | items.forEach(node => { |
| | | flat.push({ node, depth, parentArray: items, parentNode }); |
| | | if (Array.isArray(node.children) && node.children.length > 0) { |
| | | walk(node.children, depth + 1, node); |
| | | } |
| | | }); |
| | | }; |
| | | walk(dataValue.dataList, 0, null); |
| | | return flat; |
| | | }; |
| | | |
| | | const removeNodeByTempId = (items, tempId) => { |
| | | for (let i = 0; i < items.length; i++) { |
| | | if (String(items[i].tempId) === String(tempId)) { |
| | | items.splice(i, 1); |
| | | return true; |
| | | } |
| | | if ( |
| | | Array.isArray(items[i].children) && |
| | | removeNodeByTempId(items[i].children, tempId) |
| | | ) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | }; |
| | | |
| | | const updateParentRef = (node, parent) => { |
| | | node.parentTempId = parent?.tempId ?? ""; |
| | | node.parentId = parent?.id ?? ""; |
| | | }; |
| | | |
| | | const handleSortEnd = evt => { |
| | | const { oldIndex, newIndex } = evt; |
| | | if ( |
| | | typeof oldIndex !== "number" || |
| | | typeof newIndex !== "number" || |
| | | oldIndex === newIndex |
| | | ) { |
| | | return; |
| | | } |
| | | const flat = buildFlatTree(); |
| | | if (oldIndex < 0 || oldIndex >= flat.length) return; |
| | | const dragged = flat[oldIndex]; |
| | | |
| | | // ææ¾åä½äºè¢«æè¡ä¸æ¹ä¸è¡çæ°æ®(ææç» DOM é¡ºåºæ¢ç®å°åæå¹³å表) |
| | | let aEntry = null; |
| | | if (newIndex > oldIndex) { |
| | | aEntry = flat[newIndex]; |
| | | } else if (newIndex > 0) { |
| | | aEntry = flat[newIndex - 1]; |
| | | } |
| | | |
| | | if (!aEntry) { |
| | | // æå°å表æå â æä¸ºä¸çº§ |
| | | removeNodeByTempId(dataValue.dataList, dragged.node.tempId); |
| | | dataValue.dataList.unshift(dragged.node); |
| | | updateParentRef(dragged.node, null); |
| | | } else if (aEntry.depth < dragged.depth) { |
| | | // æå°æè¡æ£ä¸æ¹ â æä¸ºè¯¥è¡çå级 |
| | | removeNodeByTempId(dataValue.dataList, dragged.node.tempId); |
| | | if (!aEntry.node.children) { |
| | | aEntry.node.children = []; |
| | | } |
| | | aEntry.node.children.push(dragged.node); |
| | | updateParentRef(dragged.node, aEntry.node); |
| | | } else { |
| | | // å级æå
¥:å䏿¾ç¬¬ä¸ä¸ªæ·±åº¦ä¸è¶
è¿è¢«æè¡çèç¹ |
| | | let i = flat.indexOf(aEntry); |
| | | while (i >= 0 && flat[i].depth > dragged.depth) { |
| | | i--; |
| | | } |
| | | const eEntry = flat[i]; |
| | | // æåèªå·±çåæ å
â ä¸å¤ç(鲿¢å½¢æå¾ªç¯) |
| | | if (!eEntry || eEntry.node.tempId === dragged.node.tempId) { |
| | | return; |
| | | } |
| | | removeNodeByTempId(dataValue.dataList, dragged.node.tempId); |
| | | const siblings = eEntry.parentArray; |
| | | const idx = siblings.indexOf(eEntry.node); |
| | | siblings.splice(idx + 1, 0, dragged.node); |
| | | updateParentRef(dragged.node, eEntry.parentNode); |
| | | } |
| | | |
| | | recalculateDemandedQuantities(); |
| | | }; |
| | | |
| | | const initSortable = () => { |
| | | if (!tableRef.value) return; |
| | | const tbody = tableRef.value.$el.querySelector(".el-table__body tbody"); |
| | | if (!tbody) return; |
| | | tableSortable?.destroy(); |
| | | tableSortable = new Sortable(tbody, { |
| | | animation: 150, |
| | | handle: ".drag-handle", |
| | | disabled: !dataValue.isEdit, |
| | | ghostClass: "sortable-ghost", |
| | | onEnd: handleSortEnd, |
| | | }); |
| | | }; |
| | | |
| | | const expandAllTree = () => { |
| | | const table = tableRef.value; |
| | | if (!table) return; |
| | | const walk = items => { |
| | | items.forEach(item => { |
| | | if (Array.isArray(item.children) && item.children.length > 0) { |
| | | table.toggleRowExpansion(item, true); |
| | | walk(item.children); |
| | | } |
| | | }); |
| | | }; |
| | | walk(dataValue.dataList); |
| | | }; |
| | | |
| | | // ç¼è¾æ¨¡å¼ä¸ä¸å
许æå ,ä¿è¯ææ½æ¶ DOM è¡é¡ºåºä¸ dataList ä¸è´ |
| | | const handleTreeExpandChange = (row, expanded) => { |
| | | if (dataValue.isEdit && expanded === false) { |
| | | nextTick(() => { |
| | | tableRef.value?.toggleRowExpansion(row, true); |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // å¤å±è¡æå åå
容ä¼éæ¯,éæ°å±å¼æ¶éæ°åå§åææ½ |
| | | const handleOuterExpandChange = (row, expandedRows) => { |
| | | if (Array.isArray(expandedRows) && expandedRows.some(r => r === row)) { |
| | | nextTick(() => initSortable()); |
| | | } |
| | | }; |
| | | |
| | | watch( |
| | | () => dataValue.isEdit, |
| | | val => { |
| | | tableSortable?.option("disabled", !val); |
| | | if (val) { |
| | | nextTick(() => { |
| | | initSortable(); |
| | | expandAllTree(); |
| | | }); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | const buildSubmitTree = (items: any[]) => { |
| | | return items.map((item: any) => { |
| | | const current = { ...item }; |
| | |
| | | } |
| | | }; |
| | | |
| | | const openProcessDialog = (row: any, event?: Event) => { |
| | | // ç¹å»æ¸
餿鮿¶ä¸å¼¹çª |
| | | const target = event?.target as HTMLElement | undefined; |
| | | if (target && target.closest(".el-input__clear")) { |
| | | return; |
| | | } |
| | | dataValue.currentProcessRow = row; |
| | | dataValue.showProcessDialog = true; |
| | | }; |
| | | |
| | | const clearProcess = (row: any) => { |
| | | row.processId = ""; |
| | | row.operationId = ""; |
| | | row.processName = ""; |
| | | row.operationName = ""; |
| | | }; |
| | | |
| | | const handleProcessConfirm = (processRow: any) => { |
| | | const row = dataValue.currentProcessRow; |
| | | if (!row) { |
| | | return; |
| | | } |
| | | row.processId = processRow.id; |
| | | row.operationId = processRow.id; |
| | | row.processName = processRow.name; |
| | | row.operationName = processRow.name; |
| | | handleProcessChange(row, processRow.id); |
| | | }; |
| | | |
| | | const handleUnitQuantityChange = () => { |
| | | recalculateDemandedQuantities(); |
| | | }; |
| | |
| | | }; |
| | | |
| | | const fetchProcessOptions = async () => { |
| | | const { data } = await list({}); |
| | | const { data } = await listAll(); |
| | | console.log(data, "dataValue.dataList"); |
| | | dataValue.processOptions = normalizeListData(data); |
| | | }; |
| | |
| | | // å
å 载工åºé项ï¼åå è½½æ°æ®ï¼ç¡®ä¿el-selectè½å¤æ£ç¡®åæ¾ |
| | | await fetchProcessOptions(); |
| | | await fetchData(); |
| | | await nextTick(); |
| | | initSortable(); |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | tableSortable?.destroy(); |
| | | tableSortable = null; |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .drag-tip { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | margin-bottom: 8px; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .drag-handle { |
| | | cursor: move; |
| | | color: #909399; |
| | | text-align: center; |
| | | } |
| | | |
| | | .drag-handle:hover { |
| | | color: var(--el-color-primary); |
| | | } |
| | | |
| | | :deep(.sortable-ghost) { |
| | | opacity: 0.6; |
| | | background-color: #f5f7fa !important; |
| | | } |
| | | </style> |
| | |
| | | <div> |
| | | <el-dialog v-model="dialogVisible" |
| | | title="颿å°è´¦" |
| | | width="1350px" |
| | | width="1450px" |
| | | @close="handleClose"> |
| | | <div class="material-toolbar"> |
| | | <el-button type="primary" |
| | | plain |
| | | @click="handleAddPurchase">æ°å¢éè´</el-button> |
| | | <el-button type="primary" |
| | | @click="handleAddMaterialRow">æ°å¢</el-button> |
| | | </div> |
| | | <el-table v-loading="materialTableLoading" |
| | | :data="materialTableData" |
| | | border |
| | | @selection-change="handleMaterialSelectionChange" |
| | | row-key="tempId"> |
| | | <el-table-column align="center" |
| | | type="selection" |
| | | width="55" /> |
| | | <el-table-column label="å·¥åºåç§°" |
| | | min-width="80"> |
| | | min-width="100"> |
| | | <template #default="{ row }"> |
| | | <span v-if="row.bom === true">{{ row.operationName || "-" }}</span> |
| | | <el-select v-else |
| | |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åæåå·" |
| | | min-width="120"> |
| | | min-width="110"> |
| | | <template #default="{ row }"> |
| | | {{ row.materialModel || "-" }} |
| | | </template> |
| | | </el-table-column> |
| | | <!-- æ¹å·å¤é --> |
| | | <el-table-column min-width="200" |
| | | <el-table-column min-width="180" |
| | | label="æ¹å·"> |
| | | <template #default="{ row }"> |
| | | <el-select v-model="row.batchNo" |
| | |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="éæ±æ°é" |
| | | min-width="100"> |
| | | min-width="95"> |
| | | <template #default="{ row }"> |
| | | <span v-if="row.bom === true">{{ stripTrailingZeros(row.demandedQuantity) ?? "-" }}</span> |
| | | <el-input-number v-else |
| | |
| | | :parser="value => parseFloat(value) || 0" |
| | | style="width: 100%;" |
| | | @change="val => handleRequiredQtyChange(row, val)" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åºåä½é" |
| | | min-width="90"> |
| | | <template #default="{ row }"> |
| | | <span v-if="row.stockQty === null">-</span> |
| | | <span v-else>{{ stripTrailingZeros(row.stockQty) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="已颿°é" |
| | | min-width="90"> |
| | | <template #default="{ row }"> |
| | | {{ stripTrailingZeros(row.pickedQty) ?? 0 }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å©ä½å¯é¢" |
| | | min-width="90"> |
| | | <template #default="{ row }"> |
| | | <span :class="{ 'remaining-empty': Number(row.remainingQty) <= 0 }"> |
| | | {{ stripTrailingZeros(row.remainingQty) ?? 0 }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="计éåä½" |
| | |
| | | width="90" |
| | | fixed="right"> |
| | | <template #default="{ $index, row }"> |
| | | <el-button v-if="row.bom !== true" |
| | | <el-button v-if="row.bom !== true || Number(row.pickQty) > 0" |
| | | type="danger" |
| | | link |
| | | @click="handleDeleteMaterialRow($index)">å é¤</el-button> |
| | |
| | | <ProductSelectDialog v-model="materialProductDialogVisible" |
| | | @confirm="handleMaterialProductConfirm" |
| | | single /> |
| | | <PurchaseLedgerAddDialog ref="purchaseLedgerAddDialogRef" |
| | | v-model="purchaseLedgerDialogVisible" /> |
| | | <!-- request-url="/stockInventory/rawMaterials" --> |
| | | </div> |
| | | </template> |
| | |
| | | import { computed, ref, watch } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; |
| | | import { |
| | | findProductProcessRouteItemList, |
| | | listMain, |
| | | } from "@/api/productionManagement/productProcessRoute.js"; |
| | | import PurchaseLedgerAddDialog from "./PurchaseLedgerAddDialog.vue"; |
| | | import { findProductProcessRouteItemList } from "@/api/productionManagement/productProcessRoute.js"; |
| | | import { getStockInventoryByModelId } from "@/api/inventoryManagement/stockInventory.js"; |
| | | import { |
| | | listMaterialPickingDetail, |
| | | listMaterialPickingBom, |
| | | listMaterialPickingLedger, |
| | | saveMaterialPickingLedger, |
| | | updateMaterialPickingLedger, |
| | | } from "@/api/productionManagement/productionOrder.js"; |
| | | import { queryList2 } from "@/api/productionManagement/productStructure.js"; |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { type: Boolean, default: false }, |
| | |
| | | const materialTableLoading = ref(false); |
| | | const materialSaving = ref(false); |
| | | const materialTableData = ref([]); |
| | | const selectedMaterialRows = ref([]); |
| | | const purchaseLedgerDialogVisible = ref(false); |
| | | const purchaseLedgerAddDialogRef = ref(null); |
| | | |
| | | const isSaveDisabled = computed(() => { |
| | | if (materialTableLoading.value) return true; |
| | | if (materialTableData.value.length === 0) return true; |
| | | return !materialTableData.value.some(row => { |
| | | // æ£æ¥æ¯å¦æä»»ä½ç¨æ·è¾å
¥å
容 |
| | | const hasBatch = Array.isArray(row.batchNo) && row.batchNo.length > 0; |
| | | const hasPickQty = |
| | | row.pickQty !== null && row.pickQty !== undefined && row.pickQty !== 0; |
| | | |
| | | if (row.bom) { |
| | | // å¯¹äºæ¥èªBOMçè¡ï¼è¾å
¥æ¡åªæâæ¹å·âåâé¢ç¨æ°éâ |
| | | return hasBatch || hasPickQty; |
| | | } else { |
| | | // å¯¹äºæ°å¢è¡ï¼è¾å
¥æ¡å
æ¬âå·¥åºâãâåæâãâéæ±æ°éâãâæ¹å·âåâé¢ç¨æ°éâ |
| | | const hasOperation = !!row.operationName; |
| | | const hasMaterial = !!row.materialName; |
| | | const hasDemanded = |
| | | row.demandedQuantity !== null && |
| | | row.demandedQuantity !== undefined && |
| | | row.demandedQuantity !== 0; |
| | | return ( |
| | | hasBatch || hasPickQty || hasOperation || hasMaterial || hasDemanded |
| | | ); |
| | | } |
| | | }); |
| | | // åªæå卿¬æ¬¡é¢ç¨æ°é大äº0çè¡æå
许ä¿å |
| | | return !materialTableData.value.some(item => Number(item.pickQty) > 0); |
| | | }); |
| | | |
| | | const processOptions = ref([]); |
| | | const currentMaterialSelectRowIndex = ref(-1); |
| | | let materialTempId = 0; |
| | | |
| | | const getDefaultBatchNo = batchNoList => { |
| | | if (!Array.isArray(batchNoList) || batchNoList.length === 0) return []; |
| | | return [batchNoList[0]]; |
| | | const toNumber = val => { |
| | | const n = Number(val); |
| | | return Number.isFinite(n) ? n : 0; |
| | | }; |
| | | |
| | | // ä¿ç6ä½å°æ°ï¼é¿å
æµ®ç¹è¯¯å·® |
| | | const roundQuantity = val => Math.round(toNumber(val) * 1e6) / 1e6; |
| | | |
| | | // éæ±æ°éåç»é®ï¼ä¸åç«¯ä¿æä¸è´ï¼å·¥åºIDä¼å
ï¼ç¼ºå¤±æ¶ç¨å·¥åºåç§°ï¼ |
| | | const buildDemandKey = (technologyOperationId, operationName, productModelId) => |
| | | `${technologyOperationId != null && technologyOperationId !== undefined ? "T" + technologyOperationId : "N" + (operationName || "")}#${productModelId ?? ""}`; |
| | | |
| | | const stripTrailingZeros = val => { |
| | | if (val === null || val === undefined || val === "") return "0"; |
| | | const str = String(val); |
| | | if (str.includes(".")) { |
| | | return parseFloat(str).toString(); |
| | | } |
| | | return str; |
| | | }; |
| | | |
| | | const createMaterialRow = (row = {}) => { |
| | | const batchNoList = Array.isArray(row.batchNoList) ? row.batchNoList : []; |
| | | const batchNo = row.batchNo |
| | | ? typeof row.batchNo === "string" |
| | | ? row.batchNo.split(",") |
| | | : row.batchNo |
| | | : getDefaultBatchNo(batchNoList); |
| | | const pickedEntry = row.pickedEntry || null; |
| | | const pickedQty = pickedEntry ? roundQuantity(pickedEntry.pickedQty) : 0; |
| | | const returnedQty = pickedEntry ? roundQuantity(pickedEntry.returnedQty) : 0; |
| | | const demandedQuantity = roundQuantity(row.demandedQuantity ?? 0); |
| | | const remainingQty = Math.max(roundQuantity(demandedQuantity - pickedQty + returnedQty), 0); |
| | | |
| | | return { |
| | | tempId: row.id || `temp_${++materialTempId}`, |
| | | id: row.id, |
| | | processId: row.processId || row.technologyOperationId, |
| | | technologyOperationId: row.technologyOperationId || row.processId, |
| | | operationName: row.operationName || "", |
| | | tempId: `temp_${++materialTempId}`, |
| | | id: null, |
| | | bom: row.bom === true, |
| | | materialModelId: row.materialModelId || row.productModelId, |
| | | materialName: row.materialName || row.productName || "", |
| | | materialModel: row.materialModel || row.model || "", |
| | | demandedQuantity: Number(row.requiredQty ?? row.demandedQuantity ?? 0), |
| | | technologyOperationId: row.technologyOperationId ?? null, |
| | | operationName: row.operationName || "", |
| | | materialModelId: row.materialModelId ?? null, |
| | | materialName: row.materialName || "", |
| | | materialModel: row.materialModel || "", |
| | | unit: row.unit || "", |
| | | pickQty: Number(row.pickQty ?? row.pickQuantity ?? row.requiredQty ?? row.demandedQuantity ?? 0), |
| | | batchNo, |
| | | demandedQuantity, |
| | | pickedQty, |
| | | returnedQty, |
| | | remainingQty, |
| | | pickQty: 0, |
| | | stockQty: null, |
| | | batchNo: [], |
| | | batchNoList, |
| | | }; |
| | | }; |
| | |
| | | if (!processMap.has(key)) { |
| | | processMap.set(key, { |
| | | id: processId, |
| | | technologyOperationId: processId, |
| | | name: operationName, |
| | | }); |
| | | } |
| | | }); |
| | | processOptions.value = Array.from(processMap.values()); |
| | | }; |
| | | const isDetail = ref(true); |
| | | |
| | | const loadMaterialData = async () => { |
| | | if (!props.orderRow?.id) return; |
| | | materialTableLoading.value = true; |
| | | materialTableData.value = []; |
| | | await getProcessOptions(); |
| | | try { |
| | | const detailRes = await listMaterialPickingDetail(props.orderRow.id); |
| | | const detailList = Array.isArray(detailRes?.data) |
| | | ? detailRes.data |
| | | : detailRes?.data?.records || []; |
| | | if (detailList.length > 0) { |
| | | isDetail.value = true; |
| | | materialTableData.value = detailList.map(item => createMaterialRow(item)); |
| | | return; |
| | | } else { |
| | | isDetail.value = false; |
| | | const bomRes = await listMaterialPickingBom(props.orderRow.id); |
| | | await getProcessOptions(); |
| | | // åæ¶è·åBOMéæ±æ°æ®ä¸å·²é¢ææç»ï¼ç¨äºè®¡ç®â已颿°é/å©ä½å¯é¢â |
| | | const [bomRes, detailRes] = await Promise.all([ |
| | | listMaterialPickingBom(props.orderRow.id), |
| | | listMaterialPickingDetail(props.orderRow.id), |
| | | ]); |
| | | const bomList = Array.isArray(bomRes?.data) |
| | | ? bomRes.data |
| | | : bomRes?.data?.records || []; |
| | | materialTableData.value = bomList.map(item => createMaterialRow(item)); |
| | | return; |
| | | const detailList = Array.isArray(detailRes?.data) |
| | | ? detailRes.data |
| | | : detailRes?.data?.records || []; |
| | | |
| | | // æâå·¥åº+åæâæ±æ»åå²é¢æï¼å·²é¢æ°éãéææ°é |
| | | const pickedMap = new Map(); |
| | | detailList.forEach(item => { |
| | | if (!item) return; |
| | | const key = buildDemandKey( |
| | | item.technologyOperationId, |
| | | item.operationName, |
| | | item.productModelId |
| | | ); |
| | | const entry = pickedMap.get(key) || { |
| | | pickedQty: 0, |
| | | returnedQty: 0, |
| | | manualDemand: 0, |
| | | technologyOperationId: item.technologyOperationId, |
| | | operationName: item.operationName || "", |
| | | materialModelId: item.productModelId, |
| | | materialName: item.productName || "", |
| | | materialModel: item.model || "", |
| | | unit: item.unit || "", |
| | | batchNoList: Array.isArray(item.batchNoList) |
| | | ? item.batchNoList |
| | | : [], |
| | | }; |
| | | entry.pickedQty = roundQuantity(entry.pickedQty + toNumber(item.pickQuantity)); |
| | | entry.returnedQty = roundQuantity(entry.returnedQty + toNumber(item.returnQty)); |
| | | // æå·¥æ°å¢è¡ï¼éBOMï¼çéæ±æ°éä»¥é¢æå¿«ç
§ä¸ºå |
| | | if (item.bom !== true && item.demandedQuantity != null) { |
| | | entry.manualDemand = Math.max( |
| | | toNumber(entry.manualDemand), |
| | | toNumber(item.demandedQuantity) |
| | | ); |
| | | } |
| | | pickedMap.set(key, entry); |
| | | }); |
| | | |
| | | const rows = []; |
| | | const bomKeySet = new Set(); |
| | | // BOMè¡ï¼éæ±æ°éåBOMï¼å·²é¢æ°éååå²é¢ææ±æ» |
| | | bomList.forEach(item => { |
| | | const materialModelId = item.productModelId ?? item.materialModelId; |
| | | const key = buildDemandKey( |
| | | item.technologyOperationId, |
| | | item.operationName, |
| | | materialModelId |
| | | ); |
| | | bomKeySet.add(key); |
| | | rows.push( |
| | | createMaterialRow({ |
| | | bom: true, |
| | | technologyOperationId: item.technologyOperationId, |
| | | operationName: item.operationName || "", |
| | | materialModelId, |
| | | materialName: item.productName || item.materialName || "", |
| | | materialModel: item.model || item.materialModel || "", |
| | | unit: item.unit || "", |
| | | demandedQuantity: item.demandedQuantity ?? item.requiredQty ?? 0, |
| | | pickedEntry: pickedMap.get(key), |
| | | batchNoList: item.batchNoList || [], |
| | | }) |
| | | ); |
| | | }); |
| | | // ä¸å¨BOMä¸çåå²é¢æï¼æå·¥æ°å¢è¡ï¼ï¼ä¹å±ç¤ºåºæ¥æ¯æç»§ç»é¢æ |
| | | pickedMap.forEach((entry, key) => { |
| | | if (bomKeySet.has(key)) return; |
| | | rows.push( |
| | | createMaterialRow({ |
| | | bom: false, |
| | | technologyOperationId: entry.technologyOperationId, |
| | | operationName: entry.operationName, |
| | | materialModelId: entry.materialModelId, |
| | | materialName: entry.materialName, |
| | | materialModel: entry.materialModel, |
| | | unit: entry.unit, |
| | | demandedQuantity: entry.manualDemand, |
| | | pickedEntry: entry, |
| | | batchNoList: entry.batchNoList, |
| | | }) |
| | | ); |
| | | }); |
| | | // å
æ¥åºåºåä½éåå
¥è¡¨ï¼ä¿è¯é¦æ¬¡æå¼å³å±ç¤ºï¼rows å°æªè½¬ä¸ºååºå¼ä»£çï¼ |
| | | // å
¥è¡¨ååæ¹åå§å¯¹è±¡ä¸ä¼è§¦åè§å¾æ´æ°ï¼ |
| | | await loadStockQtyForRows(rows); |
| | | materialTableData.value = rows; |
| | | } finally { |
| | | materialTableLoading.value = false; |
| | | } |
| | |
| | | } |
| | | ); |
| | | |
| | | const stripTrailingZeros = val => { |
| | | const str = String(val); |
| | | if (str.includes(".")) { |
| | | return parseFloat(str).toString(); |
| | | } |
| | | return str; |
| | | }; |
| | | |
| | | const handleClose = () => { |
| | | materialTableData.value = []; |
| | | selectedMaterialRows.value = []; |
| | | currentMaterialSelectRowIndex.value = -1; |
| | | }; |
| | | |
| | | // æåå·æ±æ»æææ¹æ¬¡çåæå¯ç¨åºåä½é |
| | | const loadStockQtyForRows = async rows => { |
| | | const modelIds = [ |
| | | ...new Set(rows.map(row => row.materialModelId).filter(Boolean)), |
| | | ]; |
| | | const stockMap = new Map(); |
| | | await Promise.all( |
| | | modelIds.map(async modelId => { |
| | | try { |
| | | const res = await getStockInventoryByModelId(modelId); |
| | | const list = Array.isArray(res?.data) |
| | | ? res.data |
| | | : res?.data?.records || res?.data?.rows || []; |
| | | const total = list.reduce( |
| | | (sum, item) => sum + toNumber(item?.qualitity ?? item?.quantity), |
| | | 0 |
| | | ); |
| | | stockMap.set(modelId, roundQuantity(total)); |
| | | } catch (e) { |
| | | stockMap.set(modelId, 0); |
| | | } |
| | | }) |
| | | ); |
| | | rows.forEach(row => { |
| | | if (!row.materialModelId) { |
| | | row.stockQty = null; |
| | | return; |
| | | } |
| | | row.stockQty = stockMap.has(row.materialModelId) |
| | | ? stockMap.get(row.materialModelId) |
| | | : 0; |
| | | }); |
| | | }; |
| | | |
| | | const handleAddMaterialRow = () => { |
| | | materialTableData.value.push(createMaterialRow()); |
| | | }; |
| | | |
| | | const handleMaterialSelectionChange = selection => { |
| | | selectedMaterialRows.value = selection; |
| | | }; |
| | | |
| | | const handleAddPurchase = () => { |
| | | if (selectedMaterialRows.value.length === 0) { |
| | | ElMessage.warning("è¯·éæ©éè¦æ°å¢éè´çåæè¡"); |
| | | return; |
| | | } |
| | | const rowsWithModel = selectedMaterialRows.value.filter( |
| | | row => row.materialModelId |
| | | ); |
| | | if (rowsWithModel.length === 0) { |
| | | ElMessage.warning("æéè¡ç¼ºå°åæåå·ï¼è¯·å
éæ©åæååæ°å¢éè´"); |
| | | return; |
| | | } |
| | | const productRows = rowsWithModel.map(row => ({ |
| | | productModelId: row.materialModelId, |
| | | productCategory: row.materialName, |
| | | specificationModel: row.materialModel, |
| | | unit: row.unit, |
| | | isChecked: false, |
| | | })); |
| | | purchaseLedgerAddDialogRef.value?.open(productRows); |
| | | }; |
| | | |
| | | const handleDeleteMaterialRow = index => { |
| | |
| | | }; |
| | | |
| | | const handleRequiredQtyChange = (row, val) => { |
| | | const required = Number(val ?? 0); |
| | | const required = roundQuantity(val); |
| | | row.demandedQuantity = required; |
| | | if (!row.pickQty || Number(row.pickQty) === 0) { |
| | | row.pickQty = required; |
| | | } |
| | | row.remainingQty = Math.max( |
| | | roundQuantity(required - toNumber(row.pickedQty) + toNumber(row.returnedQty)), |
| | | 0 |
| | | ); |
| | | }; |
| | | |
| | | const openMaterialProductSelect = row => { |
| | |
| | | }; |
| | | |
| | | const handleMaterialProductConfirm = products => { |
| | | console.log(products, "products"); |
| | | |
| | | if (!products || products.length === 0) return; |
| | | const index = currentMaterialSelectRowIndex.value; |
| | | if (index < 0 || !materialTableData.value[index]) return; |
| | |
| | | row.materialModel = product.materialModel || product.model || ""; |
| | | row.unit = product.unit || product.measureUnit || ""; |
| | | row.batchNoList = Array.isArray(product.batchNoList) ? product.batchNoList : []; |
| | | row.batchNo = getDefaultBatchNo(row.batchNoList); |
| | | row.batchNo = []; |
| | | currentMaterialSelectRowIndex.value = -1; |
| | | materialProductDialogVisible.value = false; |
| | | loadStockQtyForRows([row]); |
| | | }; |
| | | |
| | | const validateMaterialRows = () => { |
| | | if (materialTableData.value.length === 0) { |
| | | return { valid: false, message: "请å
æ°å¢é¢ææ°æ®" }; |
| | | } |
| | | const invalidNewRow = materialTableData.value.find( |
| | | item => item.bom !== true && (!item.operationName || !item.materialName) |
| | | const pickRows = materialTableData.value.filter( |
| | | item => toNumber(item.pickQty) > 0 |
| | | ); |
| | | if (invalidNewRow) { |
| | | return { valid: false, message: "æ°å¢è¡çå·¥åºåç§°ååæå称为å¿
填项" }; |
| | | if (pickRows.length === 0) { |
| | | return { valid: false, message: "请è³å°å¡«åä¸è¡å¤§äº0çé¢ç¨æ°é" }; |
| | | } |
| | | const invalidRow = materialTableData.value.find( |
| | | item => |
| | | !item.operationName || |
| | | !item.materialName || |
| | | (Number(item.pickQty) > 0 && |
| | | (!item.batchNo || item.batchNo.length === 0)) || |
| | | (item.batchNo && item.batchNo.length > 0 && Number(item.pickQty) <= 0) || |
| | | item.demandedQuantity === null || |
| | | item.demandedQuantity === undefined || |
| | | item.pickQty === null || |
| | | item.pickQty === undefined |
| | | ); |
| | | if (invalidRow) { |
| | | if ( |
| | | invalidRow.batchNo && |
| | | invalidRow.batchNo.length > 0 && |
| | | Number(invalidRow.pickQty) <= 0 |
| | | ) { |
| | | return { valid: false, message: "éæ©äºæ¹å·æ¶ï¼é¢ç¨æ°éå¿
须大äºé¶" }; |
| | | for (let i = 0; i < materialTableData.value.length; i++) { |
| | | const item = materialTableData.value[i]; |
| | | const rowNo = i + 1; |
| | | if (toNumber(item.pickQty) <= 0) continue; |
| | | if (item.bom !== true && (!item.operationName || !item.materialName)) { |
| | | return { valid: false, message: `第${rowNo}è¡çå·¥åºåç§°ååæå称为å¿
填项` }; |
| | | } |
| | | return { valid: false, message: "请å®åå·¥åºãåæãæ¹å·åæ°éååä¿å" }; |
| | | if (!Array.isArray(item.batchNo) || item.batchNo.length === 0) { |
| | | return { valid: false, message: `第${rowNo}è¡è¯·éæ©æ¹å·` }; |
| | | } |
| | | } |
| | | return { valid: true, message: "" }; |
| | | }; |
| | |
| | | } |
| | | materialSaving.value = true; |
| | | try { |
| | | if (isDetail.value) { |
| | | await updateMaterialPickingLedger({ |
| | | productionOrderId: props.orderRow.id, |
| | | productionOrderPickDto: materialTableData.value.map(item => ({ |
| | | id: item.id, |
| | | // processId: item.operationName, |
| | | technologyOperationId: item.technologyOperationId, |
| | | operationName: item.operationName, |
| | | bom: item.bom === true, |
| | | productModelId: item.materialModelId, |
| | | // materialName: item.materialName, |
| | | // materialModel: item.materialModel, |
| | | demandedQuantity: item.demandedQuantity, |
| | | unit: item.unit, |
| | | pickQuantity: item.pickQty, |
| | | batchNo: Array.isArray(item.batchNo) |
| | | ? item.batchNo.join(",") |
| | | : item.batchNo, |
| | | })), |
| | | }); |
| | | } else { |
| | | // æ¯æ¬¡ä¿å齿¯ä¸æ¬¡ç¬ç«ç颿ï¼åªæäº¤æ¬æ¬¡é¢ç¨æ°é大äº0çè¡ï¼ |
| | | // åºåºå°è´¦ææ¬æ¬¡é¢ææ°éè®°è´¦ï¼åå²é¢æè®°å½ä¿æä¸åã |
| | | const pickRows = materialTableData.value.filter( |
| | | item => toNumber(item.pickQty) > 0 |
| | | ); |
| | | await saveMaterialPickingLedger({ |
| | | productionOrderId: props.orderRow.id, |
| | | productionOrderPickDto: materialTableData.value.map(item => ({ |
| | | id: item.id, |
| | | // processId: item.operationName, |
| | | productionOrderPickDto: pickRows.map(item => ({ |
| | | technologyOperationId: item.technologyOperationId, |
| | | operationName: item.operationName, |
| | | bom: item.bom === true, |
| | | productModelId: item.materialModelId, |
| | | // materialName: item.materialName, |
| | | // materialModel: item.materialModel, |
| | | demandedQuantity: item.demandedQuantity, |
| | | unit: item.unit, |
| | | pickQuantity: item.pickQty, |
| | | pickQuantity: toNumber(item.pickQty), |
| | | batchNo: Array.isArray(item.batchNo) |
| | | ? item.batchNo.join(",") |
| | | : item.batchNo, |
| | | })), |
| | | }); |
| | | } |
| | | |
| | | ElMessage({ message: "颿æå", type: "success" }); |
| | | emit("saved"); |
| | |
| | | margin-bottom: 12px; |
| | | text-align: right; |
| | | } |
| | | |
| | | .remaining-empty { |
| | | color: #f56c6c; |
| | | font-weight: 600; |
| | | } |
| | | </style> |
| | |
| | | </div> |
| | | <div class="info-item" |
| | | style="width: 25%;"> |
| | | <span class="label">éæ±æ¥æï¼</span> |
| | | <span class="label">äº¤è´§æ¥æï¼</span> |
| | | <span class="value">{{ formatDate(orderRow?.planCompleteTime) }}</span> |
| | | </div> |
| | | </div> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div> |
| | | <FormDialog v-model="dialogVisible" |
| | | title="æ°å¢éè´å°è´¦é¡µé¢" |
| | | :width="'70%'" |
| | | operation-type="add" |
| | | @close="closeDia" |
| | | @confirm="submitForm" |
| | | @cancel="closeDia"> |
| | | <el-form :model="form" |
| | | label-width="140px" |
| | | label-position="top" |
| | | :rules="rules" |
| | | ref="formRef"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="éè´ååå·ï¼" prop="purchaseContractNumber"> |
| | | <el-input v-model="form.purchaseContractNumber" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col v-if="showSalesContractBinding" :span="12"> |
| | | <el-form-item label="éå®ååå·ï¼" prop="salesLedgerId"> |
| | | <el-select v-model="form.salesLedgerId" |
| | | placeholder="è¯·éæ©" |
| | | filterable |
| | | clearable |
| | | @change="salesLedgerChange"> |
| | | <el-option v-for="item in salesContractList" |
| | | :key="item.id" |
| | | :label="item.salesContractNo" |
| | | :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="supplierId"> |
| | | <el-select v-model="form.supplierId" placeholder="è¯·éæ©" filterable clearable> |
| | | <el-option v-for="item in supplierList" |
| | | :key="item.id" |
| | | :label="item.supplierName" |
| | | :value="item.id"> |
| | | {{ item.supplierName }} |
| | | </el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="项ç®åç§°" prop="projectName"> |
| | | <el-input v-model="form.projectName" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="仿¬¾æ¹å¼"> |
| | | <el-input v-model="form.paymentMethod" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç¾è®¢æ¥æï¼" prop="executionDate"> |
| | | <el-date-picker v-model="form.executionDate" |
| | | style="width: 100%" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | type="date" |
| | | placeholder="è¯·éæ©" |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å½å
¥äººï¼" prop="recorderId"> |
| | | <el-select v-model="form.recorderId" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | filterable |
| | | disabled> |
| | | <el-option v-for="item in userList" |
| | | :key="item.userId" |
| | | :label="item.nickName" |
| | | :value="item.userId" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å½å
¥æ¥æï¼" prop="entryDate"> |
| | | <el-date-picker v-model="form.entryDate" |
| | | style="width: 100%" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | type="date" |
| | | placeholder="è¯·éæ©" |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row> |
| | | <el-form-item label="产åä¿¡æ¯ï¼"> |
| | | <el-button type="primary" @click="openProductForm('add')">æ·»å </el-button> |
| | | <el-button plain type="danger" @click="deleteProduct">å é¤</el-button> |
| | | </el-form-item> |
| | | <div class="select-button-group"> |
| | | <el-select v-model="templateName" |
| | | filterable |
| | | allow-create |
| | | :reserve-keyword="true" |
| | | :default-first-option="false" |
| | | clearable |
| | | :input-value="filterInputValue" |
| | | @filter-change="onTemplateFilterChange" |
| | | @change="onTemplateChange" |
| | | @focus="getTemplateList" |
| | | placeholder="è¯·éæ©æ¨¡çæè
è¾å
¥æ°ç模çåç§°åéæ©" |
| | | class="no-arrow-select template-select"> |
| | | <el-option v-for="item in templateList" |
| | | :key="item.id || item.value" |
| | | :label="item.templateName" |
| | | :value="item.templateName"> |
| | | <div class="template-option"> |
| | | <span>{{ item.templateName }}</span> |
| | | <el-icon v-if="item.id" |
| | | class="delete-icon" |
| | | @click.stop="handleDeleteTemplate(item)"> |
| | | <Delete /> |
| | | </el-icon> |
| | | </div> |
| | | </el-option> |
| | | </el-select> |
| | | <el-button size="small" |
| | | class="template-save-button" |
| | | :disabled="!templateName || templateName.trim() === '' || (!currentTemplateId && isTemplateNameDuplicate)" |
| | | @click="handleButtonClick"> |
| | | ä¿å |
| | | </el-button> |
| | | </div> |
| | | </el-row> |
| | | <el-table :data="productData" |
| | | border |
| | | show-summary |
| | | :summary-method="summarizeProTable" |
| | | @selection-change="productSelected"> |
| | | <el-table-column align="center" type="selection" width="55" /> |
| | | <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" width="70" /> |
| | | <el-table-column label="æ°é" prop="quantity" width="70" /> |
| | | <el-table-column label="ç¨ç(%)" prop="taxRate" width="80" /> |
| | | <el-table-column label="å«ç¨åä»·(å
)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" width="150" /> |
| | | <el-table-column label="å«ç¨æ»ä»·(å
)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" width="150" /> |
| | | <el-table-column label="ä¸å«ç¨æ»ä»·(å
)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" width="150" /> |
| | | <el-table-column label="æ¯å¦è´¨æ£" prop="isChecked" width="150"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="row.isChecked ? 'success' : 'info'"> |
| | | {{ row.isChecked ? 'æ¯' : 'å¦' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column fixed="right" label="æä½" min-width="60" align="center"> |
| | | <template #default="{ row, $index }"> |
| | | <el-button link type="primary" @click="openProductForm('edit', row, $index)">ç¼è¾</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="夿³¨Â·ï¼" prop="remarks"> |
| | | <el-input v-model="form.remarks" placeholder="请è¾å
¥" clearable type="textarea" :rows="2" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="éä»¶ææï¼" prop="purchaseLedgerFiles"> |
| | | <FileUpload v-model:file-list="fileList" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | </FormDialog> |
| | | |
| | | <FormDialog v-model="productFormVisible" |
| | | :title="productOperationType === 'add' ? 'æ°å¢äº§å' : 'ç¼è¾äº§å'" |
| | | :width="'40%'" |
| | | :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="12"> |
| | | <el-form-item label="产å大类ï¼" prop="productCategory"> |
| | | <el-input v-model="productForm.productCategory" |
| | | placeholder="ç¹å»éæ©" |
| | | readonly |
| | | style="cursor:pointer" |
| | | @click="showProductSelectDialog = true" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è§æ ¼åå·ï¼" prop="productModelId"> |
| | | <el-input v-model="productForm.specificationModel" |
| | | placeholder="ç¹å»éæ©" |
| | | readonly |
| | | style="cursor:pointer" |
| | | @click="showProductSelectDialog = true" /> |
| | | </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-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç¨ç(%)ï¼" prop="taxRate"> |
| | | <el-select v-model="productForm.taxRate" placeholder="è¯·éæ©" clearable @change="mathNum"> |
| | | <el-option v-for="dict in tax_rate" |
| | | :key="dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å«ç¨åä»·(å
)ï¼" prop="taxInclusiveUnitPrice"> |
| | | <el-input-number v-model="productForm.taxInclusiveUnitPrice" |
| | | :precision="6" |
| | | :step="0.000001" |
| | | :min="0" |
| | | clearable |
| | | style="width: 100%" |
| | | @change="mathNum" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ°éï¼" prop="quantity"> |
| | | <el-input-number v-model="productForm.quantity" |
| | | :step="0.1" |
| | | clearable |
| | | :precision="2" |
| | | :min="0" |
| | | style="width: 100%" |
| | | placeholder="请è¾å
¥" |
| | | @change="mathNum" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å«ç¨æ»ä»·(å
)ï¼" prop="taxInclusiveTotalPrice"> |
| | | <el-input-number v-model="productForm.taxInclusiveTotalPrice" |
| | | :precision="6" |
| | | :step="0.000001" |
| | | :min="0" |
| | | clearable |
| | | style="width: 100%" |
| | | @change="reverseMathNum('taxInclusiveTotalPrice')" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¸å«ç¨æ»ä»·(å
)ï¼" prop="taxExclusiveTotalPrice"> |
| | | <el-input-number v-model="productForm.taxExclusiveTotalPrice" |
| | | :precision="6" |
| | | :step="0.000001" |
| | | :min="0" |
| | | clearable |
| | | style="width: 100%" |
| | | @change="reverseMathNum('taxExclusiveTotalPrice')" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å票类åï¼" prop="invoiceType"> |
| | | <el-select v-model="productForm.invoiceType" placeholder="è¯·éæ©" clearable> |
| | | <el-option label="墿®ç¥¨" value="墿®ç¥¨" /> |
| | | <el-option label="å¢ä¸ç¥¨" value="å¢ä¸ç¥¨" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ¯å¦è´¨æ£ï¼" prop="isChecked"> |
| | | <el-radio-group v-model="productForm.isChecked"> |
| | | <el-radio label="æ¯" :value="true" /> |
| | | <el-radio label="å¦" :value="false" /> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | </FormDialog> |
| | | |
| | | <ProductSelectDialog v-model="showProductSelectDialog" |
| | | @confirm="handleProductSelect" |
| | | single /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, getCurrentInstance, nextTick, reactive, ref, toRefs } from "vue"; |
| | | import { Delete } from "@element-plus/icons-vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import FileUpload from "@/components/AttachmentUpload/file/index.vue"; |
| | | import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { userListNoPage } from "@/api/system/user.js"; |
| | | import { getProductInfoByContractNo } from "@/api/salesManagement/salesLedger.js"; |
| | | import { |
| | | addOrEditPurchase, |
| | | addPurchaseTemplate, |
| | | createPurchaseNo, |
| | | delPurchaseTemplate, |
| | | getOptions, |
| | | getPurchaseTemplateList, |
| | | getSalesNo, |
| | | updatePurchaseTemplate, |
| | | } from "@/api/procurementManagement/procurementLedger.js"; |
| | | import { tableAmountFormatter, formatDecimal, buildAmountSummaryFormat } from "@/utils/numberFormat"; |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { type: Boolean, default: false }, |
| | | initialProducts: { type: Array, default: () => [] }, |
| | | }); |
| | | const emit = defineEmits(["update:modelValue", "saved"]); |
| | | |
| | | const dialogVisible = computed({ |
| | | get: () => props.modelValue, |
| | | set: val => emit("update:modelValue", val), |
| | | }); |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | const { tax_rate } = proxy.useDict("tax_rate"); |
| | | const userStore = useUserStore(); |
| | | const showSalesContractBinding = false; |
| | | |
| | | const productData = ref([]); |
| | | const productSelectedRows = ref([]); |
| | | const userList = ref([]); |
| | | const salesContractList = ref([]); |
| | | const supplierList = ref([]); |
| | | const fileList = ref([]); |
| | | const productFormVisible = ref(false); |
| | | const showProductSelectDialog = ref(false); |
| | | const productOperationType = ref(""); |
| | | const productOperationIndex = ref(-1); |
| | | const templateName = ref(""); |
| | | const filterInputValue = ref(""); |
| | | const templateList = ref([]); |
| | | const isTemplateNameDuplicate = ref(false); |
| | | const currentTemplateId = ref(null); |
| | | let duplicateCheckTimer = null; |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | purchaseContractNumber: "", |
| | | salesLedgerId: "", |
| | | projectName: "", |
| | | recorderId: "", |
| | | entryDate: "", |
| | | productData: [], |
| | | supplierName: "", |
| | | supplierId: "", |
| | | paymentMethod: "", |
| | | executionDate: "", |
| | | isChecked: false, |
| | | }, |
| | | rules: { |
| | | purchaseContractNumber: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | projectName: [{ required: true, message: "请è¾å
¥é¡¹ç®åç§°", trigger: "blur" }], |
| | | supplierId: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | entryDate: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | executionDate: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | }, |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | | |
| | | const productFormData = reactive({ |
| | | productForm: { |
| | | productId: "", |
| | | productCategory: "", |
| | | productModelId: "", |
| | | specificationModel: "", |
| | | unit: "", |
| | | quantity: "", |
| | | taxInclusiveUnitPrice: "", |
| | | taxRate: "", |
| | | taxInclusiveTotalPrice: "", |
| | | taxExclusiveTotalPrice: "", |
| | | invoiceType: "", |
| | | warnNum: "", |
| | | isChecked: false, |
| | | }, |
| | | productRules: { |
| | | productCategory: [{ required: true, message: "è¯·éæ©äº§å大类", trigger: "change" }], |
| | | productModelId: [{ required: true, message: "è¯·éæ©è§æ ¼åå·", trigger: "change" }], |
| | | unit: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | quantity: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | taxInclusiveUnitPrice: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | taxRate: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | taxInclusiveTotalPrice: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | taxExclusiveTotalPrice: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | invoiceType: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | isChecked: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | }, |
| | | }); |
| | | const { productForm, productRules } = toRefs(productFormData); |
| | | |
| | | const formattedNumber = tableAmountFormatter; |
| | | |
| | | const getCurrentDate = () => { |
| | | const today = new Date(); |
| | | const year = today.getFullYear(); |
| | | const month = String(today.getMonth() + 1).padStart(2, "0"); |
| | | const day = String(today.getDate()).padStart(2, "0"); |
| | | return `${year}-${month}-${day}`; |
| | | }; |
| | | |
| | | const normalizeProductRow = row => ({ |
| | | productId: row.productId || "", |
| | | productCategory: row.productCategory || row.materialName || row.productName || "", |
| | | productModelId: row.productModelId || row.materialModelId || row.modelId || "", |
| | | specificationModel: row.specificationModel || row.materialModel || row.model || "", |
| | | unit: row.unit || "", |
| | | quantity: row.quantity || "", |
| | | taxInclusiveUnitPrice: row.taxInclusiveUnitPrice || "", |
| | | taxRate: row.taxRate || "", |
| | | taxInclusiveTotalPrice: row.taxInclusiveTotalPrice || "", |
| | | taxExclusiveTotalPrice: row.taxExclusiveTotalPrice || "", |
| | | invoiceType: row.invoiceType || "", |
| | | warnNum: row.warnNum || "", |
| | | isChecked: row.isChecked ?? false, |
| | | }); |
| | | |
| | | const resetDialogState = () => { |
| | | form.value = { |
| | | purchaseContractNumber: "", |
| | | salesLedgerId: "", |
| | | projectName: "", |
| | | recorderId: "", |
| | | entryDate: "", |
| | | productData: [], |
| | | supplierName: "", |
| | | supplierId: "", |
| | | paymentMethod: "", |
| | | executionDate: "", |
| | | isChecked: false, |
| | | }; |
| | | productData.value = []; |
| | | productSelectedRows.value = []; |
| | | fileList.value = []; |
| | | templateName.value = ""; |
| | | filterInputValue.value = ""; |
| | | isTemplateNameDuplicate.value = false; |
| | | currentTemplateId.value = null; |
| | | }; |
| | | |
| | | const open = async products => { |
| | | resetDialogState(); |
| | | await Promise.all([ |
| | | getTemplateList(), |
| | | userListNoPage().then(res => { |
| | | userList.value = res.data || []; |
| | | }), |
| | | getSalesNo().then(res => { |
| | | salesContractList.value = res || []; |
| | | }), |
| | | getOptions().then(res => { |
| | | supplierList.value = (res.data || []).filter(item => item.isWhite === 0); |
| | | }), |
| | | ]); |
| | | |
| | | form.value.entryDate = getCurrentDate(); |
| | | form.value.recorderId = userStore.id; |
| | | try { |
| | | const purchaseNoRes = await createPurchaseNo(); |
| | | if (purchaseNoRes?.data) { |
| | | form.value.purchaseContractNumber = purchaseNoRes.data; |
| | | } |
| | | } catch (error) { |
| | | console.error("çæéè´ååå·å¤±è´¥:", error); |
| | | proxy.$modal.msgWarning("çæéè´ååå·å¤±è´¥"); |
| | | } |
| | | |
| | | const sourceProducts = Array.isArray(products) ? products : props.initialProducts; |
| | | productData.value = sourceProducts.map(normalizeProductRow); |
| | | dialogVisible.value = true; |
| | | await nextTick(); |
| | | if (productData.value.length > 0) { |
| | | ElMessage.warning("å·²ææé颿è¡çæäº§åä¿¡æ¯ï¼è¯·ç¼è¾è¡¥å
¨æ°éãç¨çãåä»·ãå票类åçå¿
填项ååæäº¤éè´å°è´¦"); |
| | | } |
| | | }; |
| | | |
| | | defineExpose({ open }); |
| | | |
| | | const checkTemplateNameDuplicate = name => { |
| | | if (!name || name.trim() === "") { |
| | | isTemplateNameDuplicate.value = false; |
| | | return false; |
| | | } |
| | | const isDuplicate = templateList.value.some(item => item.templateName === name.trim()); |
| | | isTemplateNameDuplicate.value = isDuplicate; |
| | | return isDuplicate; |
| | | }; |
| | | |
| | | const onTemplateFilterChange = val => { |
| | | filterInputValue.value = val ?? ""; |
| | | if (duplicateCheckTimer) { |
| | | clearTimeout(duplicateCheckTimer); |
| | | } |
| | | if (val && val.trim()) { |
| | | duplicateCheckTimer = setTimeout(() => { |
| | | if (checkTemplateNameDuplicate(val)) { |
| | | ElMessage.warning("模æ¿åç§°å·²åå¨ï¼è¯·æ´æ¢æ¨¡æ¿åç§°"); |
| | | } |
| | | }, 300); |
| | | } else { |
| | | isTemplateNameDuplicate.value = false; |
| | | } |
| | | }; |
| | | |
| | | const onTemplateChange = async val => { |
| | | if (typeof val === "string") { |
| | | filterInputValue.value = val; |
| | | checkTemplateNameDuplicate(val); |
| | | } |
| | | const matchedTemplate = templateList.value.find(item => item.templateName === val); |
| | | if (matchedTemplate?.id) { |
| | | currentTemplateId.value = matchedTemplate.id; |
| | | isTemplateNameDuplicate.value = false; |
| | | form.value.supplierId = matchedTemplate.supplierId || form.value.supplierId; |
| | | form.value.supplierName = matchedTemplate.supplierName || form.value.supplierName; |
| | | form.value.projectName = matchedTemplate.projectName || form.value.projectName; |
| | | form.value.paymentMethod = matchedTemplate.paymentMethod || form.value.paymentMethod; |
| | | productData.value = matchedTemplate.productList || matchedTemplate.productData || []; |
| | | } else { |
| | | currentTemplateId.value = null; |
| | | } |
| | | }; |
| | | |
| | | const getTemplateList = async () => { |
| | | const res = await getPurchaseTemplateList(); |
| | | if (res && res.code === 200 && Array.isArray(res.data)) { |
| | | templateList.value = res.data; |
| | | } |
| | | }; |
| | | |
| | | const handleButtonClick = async () => { |
| | | if (!templateName.value || templateName.value.trim() === "") { |
| | | ElMessage.warning("请è¾å
¥æ¨¡æ¿åç§°"); |
| | | return; |
| | | } |
| | | if (!currentTemplateId.value && checkTemplateNameDuplicate(templateName.value)) { |
| | | ElMessage.warning("模æ¿åç§°å·²åå¨ï¼è¯·æ´æ¢æ¨¡æ¿åç§°"); |
| | | return; |
| | | } |
| | | if (!form.value.supplierId) { |
| | | ElMessage.warning("请å
éæ©ä¾åºå"); |
| | | return; |
| | | } |
| | | if (!productData.value || productData.value.length === 0) { |
| | | ElMessage.warning("请å
æ·»å 产åä¿¡æ¯"); |
| | | return; |
| | | } |
| | | |
| | | const params = { |
| | | productData: proxy.HaveJson(productData.value), |
| | | supplierId: form.value.supplierId, |
| | | paymentMethod: form.value.paymentMethod, |
| | | recorderId: form.value.recorderId, |
| | | projectName: form.value.projectName, |
| | | templateName: templateName.value.trim(), |
| | | }; |
| | | const res = currentTemplateId.value |
| | | ? await updatePurchaseTemplate({ id: currentTemplateId.value, ...params }) |
| | | : await addPurchaseTemplate(params); |
| | | if (res && res.code === 200) { |
| | | ElMessage.success(currentTemplateId.value ? "æ¨¡æ¿æ´æ°æå" : "模æ¿ä¿åæå"); |
| | | await getTemplateList(); |
| | | templateName.value = ""; |
| | | filterInputValue.value = ""; |
| | | isTemplateNameDuplicate.value = false; |
| | | currentTemplateId.value = null; |
| | | } else { |
| | | ElMessage.error(res?.msg || "模æ¿ä¿å失败"); |
| | | } |
| | | }; |
| | | |
| | | const productSelected = selectedRows => { |
| | | productSelectedRows.value = selectedRows; |
| | | }; |
| | | |
| | | const openProductForm = (type, row, index) => { |
| | | productOperationType.value = type; |
| | | productOperationIndex.value = index ?? -1; |
| | | proxy.resetForm("productFormRef"); |
| | | productForm.value = type === "edit" ? { ...row } : { isChecked: false }; |
| | | productFormVisible.value = true; |
| | | }; |
| | | |
| | | const handleProductSelect = products => { |
| | | if (!products || !products.length) return; |
| | | const p = products[0]; |
| | | productForm.value.productId = p.productId || ""; |
| | | productForm.value.productCategory = p.productName || p.topProductName || ""; |
| | | productForm.value.productModelId = p.id; |
| | | productForm.value.specificationModel = p.model || ""; |
| | | productForm.value.unit = p.unit || ""; |
| | | showProductSelectDialog.value = false; |
| | | }; |
| | | |
| | | const closeProductDia = () => { |
| | | proxy.resetForm("productFormRef"); |
| | | productFormVisible.value = false; |
| | | }; |
| | | |
| | | const submitProduct = () => { |
| | | proxy.$refs["productFormRef"].validate(valid => { |
| | | if (!valid) return; |
| | | if (productOperationType.value === "add") { |
| | | productData.value.push({ ...productForm.value }); |
| | | } else if (productOperationIndex.value > -1) { |
| | | productData.value[productOperationIndex.value] = { ...productForm.value }; |
| | | } |
| | | closeProductDia(); |
| | | }); |
| | | }; |
| | | |
| | | const deleteProduct = () => { |
| | | if (productSelectedRows.value.length === 0) { |
| | | proxy.$modal.msgWarning("è¯·éæ©æ°æ®"); |
| | | return; |
| | | } |
| | | productData.value = productData.value.filter(item => !productSelectedRows.value.includes(item)); |
| | | productSelectedRows.value = []; |
| | | }; |
| | | |
| | | const summarizeProTable = param => proxy.summarizeTable( |
| | | param, |
| | | ["taxInclusiveUnitPrice", "taxInclusiveTotalPrice", "taxExclusiveTotalPrice"], |
| | | buildAmountSummaryFormat(["taxInclusiveUnitPrice", "taxInclusiveTotalPrice", "taxExclusiveTotalPrice"]) |
| | | ); |
| | | |
| | | const mathNum = () => { |
| | | if (!productForm.value.taxRate) { |
| | | proxy.$modal.msgWarning("请å
éæ©ç¨ç"); |
| | | return; |
| | | } |
| | | if (!productForm.value.taxInclusiveUnitPrice || !productForm.value.quantity) return; |
| | | productForm.value.taxInclusiveTotalPrice = proxy.calculateTaxIncludeTotalPrice( |
| | | productForm.value.taxInclusiveUnitPrice, |
| | | productForm.value.quantity |
| | | ); |
| | | productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice( |
| | | productForm.value.taxInclusiveTotalPrice, |
| | | productForm.value.taxRate |
| | | ); |
| | | }; |
| | | |
| | | const reverseMathNum = field => { |
| | | if (!productForm.value.taxRate) { |
| | | proxy.$modal.msgWarning("请å
éæ©ç¨ç"); |
| | | return; |
| | | } |
| | | const taxRate = Number(productForm.value.taxRate); |
| | | if (!taxRate) return; |
| | | if (field === "taxInclusiveTotalPrice") { |
| | | if (productForm.value.quantity) { |
| | | productForm.value.taxInclusiveUnitPrice = formatDecimal( |
| | | Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.quantity) |
| | | ); |
| | | } else if (productForm.value.taxInclusiveUnitPrice) { |
| | | productForm.value.quantity = ( |
| | | Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.taxInclusiveUnitPrice) |
| | | ).toFixed(2); |
| | | } |
| | | productForm.value.taxExclusiveTotalPrice = formatDecimal( |
| | | Number(productForm.value.taxInclusiveTotalPrice) / (1 + taxRate / 100) |
| | | ); |
| | | } else if (field === "taxExclusiveTotalPrice") { |
| | | productForm.value.taxInclusiveTotalPrice = formatDecimal( |
| | | Number(productForm.value.taxExclusiveTotalPrice) * (1 + taxRate / 100) |
| | | ); |
| | | if (productForm.value.quantity) { |
| | | productForm.value.taxInclusiveUnitPrice = formatDecimal( |
| | | Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.quantity) |
| | | ); |
| | | } else if (productForm.value.taxInclusiveUnitPrice) { |
| | | productForm.value.quantity = ( |
| | | Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.taxInclusiveUnitPrice) |
| | | ).toFixed(2); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const salesLedgerChange = async () => { |
| | | await querygProductInfoByContractNo(); |
| | | }; |
| | | |
| | | const querygProductInfoByContractNo = async () => { |
| | | const { code, data } = await getProductInfoByContractNo({ |
| | | contractNo: form.value.salesLedgerId, |
| | | }); |
| | | if (code === 200) { |
| | | productData.value = data; |
| | | } |
| | | }; |
| | | |
| | | const validateProductRows = () => { |
| | | if (productData.value.length === 0) { |
| | | return "请添å 产åä¿¡æ¯"; |
| | | } |
| | | const requiredFields = [ |
| | | ["productCategory", "产å大类"], |
| | | ["productModelId", "è§æ ¼åå·"], |
| | | ["unit", "åä½"], |
| | | ["quantity", "æ°é"], |
| | | ["taxRate", "ç¨ç"], |
| | | ["taxInclusiveUnitPrice", "å«ç¨åä»·"], |
| | | ["taxInclusiveTotalPrice", "å«ç¨æ»ä»·"], |
| | | ["taxExclusiveTotalPrice", "ä¸å«ç¨æ»ä»·"], |
| | | ["invoiceType", "å票类å"], |
| | | ]; |
| | | for (let i = 0; i < productData.value.length; i++) { |
| | | const row = productData.value[i]; |
| | | const missing = requiredFields.find(([field]) => row[field] === "" || row[field] === null || row[field] === undefined); |
| | | if (missing) { |
| | | return `第${i + 1}è¡äº§åä¿¡æ¯ç¼ºå°${missing[1]}ï¼è¯·ç¼è¾å®åæäº¤`; |
| | | } |
| | | } |
| | | return ""; |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (!valid) return; |
| | | const productMessage = validateProductRows(); |
| | | if (productMessage) { |
| | | proxy.$modal.msgWarning(productMessage); |
| | | return; |
| | | } |
| | | |
| | | const processedProductData = productData.value.map(product => { |
| | | const { id, ...rest } = product; |
| | | return rest; |
| | | }); |
| | | const submitData = { |
| | | ...form.value, |
| | | productData: proxy.HaveJson(processedProductData), |
| | | storageBlobDTOS: fileList.value, |
| | | type: 2, |
| | | }; |
| | | if (!submitData.salesLedgerId) { |
| | | submitData.salesContractNo = ""; |
| | | } |
| | | delete submitData.id; |
| | | |
| | | addOrEditPurchase(submitData).then(() => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | emit("saved"); |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | | dialogVisible.value = false; |
| | | }; |
| | | |
| | | const handleDeleteTemplate = async item => { |
| | | if (!item.id) { |
| | | proxy.$modal.msgWarning("æ æ³å é¤è¯¥æ¨¡æ¿"); |
| | | return; |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm(`ç¡®å®è¦å 餿¨¡æ¿"${item.templateName}"åï¼`, "å é¤ç¡®è®¤", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }); |
| | | const res = await delPurchaseTemplate([item.id]); |
| | | if (res && res.code === 200) { |
| | | ElMessage.success("å 餿å"); |
| | | if (templateName.value === item.templateName) { |
| | | templateName.value = ""; |
| | | filterInputValue.value = ""; |
| | | } |
| | | await getTemplateList(); |
| | | } else { |
| | | ElMessage.error(res?.msg || "å é¤å¤±è´¥"); |
| | | } |
| | | } catch (error) { |
| | | if (error !== "cancel") { |
| | | ElMessage.error("å é¤å¤±è´¥ï¼è¯·ç¨åéè¯"); |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .el-row { |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .select-button-group { |
| | | display: flex; |
| | | align-items: center; |
| | | width: 500px; |
| | | margin: 20px 0; |
| | | } |
| | | |
| | | .template-select { |
| | | width: 500px; |
| | | } |
| | | |
| | | .template-save-button { |
| | | height: 32px; |
| | | margin-left: 8px; |
| | | } |
| | | |
| | | .template-option { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .no-arrow-select { |
| | | --el-select-suffix-icon-color: transparent; |
| | | } |
| | | |
| | | .delete-icon { |
| | | cursor: pointer; |
| | | color: #f56c6c; |
| | | font-size: 14px; |
| | | margin-left: 8px; |
| | | transition: all 0.3s; |
| | | |
| | | &:hover { |
| | | color: #f56c6c !important; |
| | | transform: scale(1.2); |
| | | } |
| | | } |
| | | </style> |
| | |
| | | style="width: 160px;" |
| | | @change="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="éå®åå·:"> |
| | | <el-input v-model="searchForm.salesContractNo" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | prefix-icon="Search" |
| | | style="width: 160px;" |
| | | @change="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="产ååç§°:"> |
| | | <el-input v-model="searchForm.productName" |
| | | placeholder="请è¾å
¥" |
| | |
| | | <div class="info-value">{{ item.unit || '-' }}</div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="info-label">éæ±æ¥æ</div> |
| | | <div class="info-label">äº¤è´§æ¥æ</div> |
| | | <div class="info-value">{{ item.requiredDate ? dayjs(item.requiredDate).format('YYYY-MM-DD') : '-' }}</div> |
| | | </div> |
| | | </div> |
| | |
| | | </el-dialog> |
| | | <MaterialLedgerDialog v-model="materialDialogVisible" |
| | | :order-row="currentMaterialOrder" |
| | | @saved="getList" |
| | | /> |
| | | <MaterialDetailDialog v-model="materialDetailDialogVisible" |
| | | :order-row="currentMaterialDetailOrder" |
| | |
| | | prop: "npsNo", |
| | | width: "150px", |
| | | }, |
| | | { |
| | | label: "éå®åå·", |
| | | prop: "salesContractNo", |
| | | width: "150px", |
| | | }, |
| | | // 1.å¾
å¼å§ã2.è¿è¡ä¸ã3.已宿ã4.已忶ã5.å·²ç»æ |
| | | { |
| | | label: "ç¶æ", |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog v-model="visible" title="鿩工åº" width="800px" destroy-on-close :close-on-click-modal="false"> |
| | | <el-form :inline="true" :model="query" class="mb-2"> |
| | | <el-form-item label="å·¥åºåç§°"> |
| | | <el-input v-model="query.name" placeholder="è¾å
¥å·¥åºåç§°" clearable @keyup.enter="onSearch" style="width: 180px" /> |
| | | </el-form-item> |
| | | <el-form-item label="å·¥åºç¼ç "> |
| | | <el-input v-model="query.no" placeholder="è¾å
¥å·¥åºç¼ç " clearable @keyup.enter="onSearch" style="width: 180px" /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="onSearch">æç´¢</el-button> |
| | | <el-button @click="onReset">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <el-table ref="tableRef" v-loading="loading" :data="tableData" height="380" highlight-current-row row-key="id" |
| | | @current-change="handleCurrentChange" @row-dblclick="onConfirm"> |
| | | <el-table-column type="index" label="åºå·" width="60" /> |
| | | <el-table-column prop="no" label="å·¥åºç¼ç " min-width="130" /> |
| | | <el-table-column prop="name" label="å·¥åºåç§°" min-width="150" /> |
| | | <el-table-column prop="isQuality" label="æ¯å¦è´¨æ£" min-width="90"> |
| | | <template #default="{ row }"> |
| | | <el-tag size="small" :type="row.isQuality ? 'warning' : 'info'">{{ row.isQuality ? 'æ¯' : 'å¦' }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="isProduction" label="æ¯å¦ç产" min-width="90"> |
| | | <template #default="{ row }"> |
| | | <el-tag size="small" :type="row.isProduction ? 'success' : 'info'">{{ row.isProduction ? 'æ¯' : 'å¦' }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="remark" label="夿³¨" min-width="120" show-overflow-tooltip /> |
| | | </el-table> |
| | | |
| | | <div class="mt-3 flex justify-end"> |
| | | <el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total" |
| | | v-model:page-size="page.pageSize" v-model:current-page="page.pageNum" :page-sizes="[10, 20, 50, 100]" |
| | | @size-change="onPageChange" @current-change="onPageChange" /> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <el-button type="primary" :disabled="!selectedRow" @click="onConfirm">ç¡®å®</el-button> |
| | | <el-button @click="close()">åæ¶</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { computed, reactive, ref, watch } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { listPage } from "@/api/productionManagement/productionProcess"; |
| | | |
| | | export type ProcessRow = { |
| | | id: number; |
| | | no?: string; |
| | | name: string; |
| | | isQuality?: boolean; |
| | | isProduction?: boolean; |
| | | remark?: string; |
| | | }; |
| | | |
| | | const props = defineProps<{ |
| | | modelValue: boolean; |
| | | }>(); |
| | | |
| | | const emit = defineEmits(["update:modelValue", "confirm"]); |
| | | |
| | | const visible = computed({ |
| | | get: () => props.modelValue, |
| | | set: v => emit("update:modelValue", v), |
| | | }); |
| | | |
| | | const query = reactive({ |
| | | name: "", |
| | | no: "", |
| | | }); |
| | | |
| | | const page = reactive({ |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | }); |
| | | |
| | | const loading = ref(false); |
| | | const tableData = ref<ProcessRow[]>([]); |
| | | const total = ref(0); |
| | | const selectedRow = ref<ProcessRow | null>(null); |
| | | const tableRef = ref(); |
| | | |
| | | function close() { |
| | | visible.value = false; |
| | | } |
| | | |
| | | function handleCurrentChange(row: ProcessRow) { |
| | | selectedRow.value = row; |
| | | } |
| | | |
| | | function onSearch() { |
| | | page.pageNum = 1; |
| | | loadData(); |
| | | } |
| | | |
| | | function onReset() { |
| | | query.name = ""; |
| | | query.no = ""; |
| | | onSearch(); |
| | | } |
| | | |
| | | function onPageChange() { |
| | | loadData(); |
| | | } |
| | | |
| | | function onConfirm() { |
| | | if (!selectedRow.value) { |
| | | ElMessage.warning("è¯·éæ©ä¸ä¸ªå·¥åº"); |
| | | return; |
| | | } |
| | | emit("confirm", selectedRow.value); |
| | | close(); |
| | | } |
| | | |
| | | async function loadData() { |
| | | loading.value = true; |
| | | try { |
| | | selectedRow.value = null; |
| | | if (tableRef.value) { |
| | | tableRef.value.setCurrentRow(null); |
| | | } |
| | | const { data } = await listPage({ |
| | | current: page.pageNum, |
| | | size: page.pageSize, |
| | | name: query.name.trim(), |
| | | no: query.no.trim(), |
| | | }); |
| | | const records = data?.records ?? (Array.isArray(data) ? data : []); |
| | | tableData.value = Array.isArray(records) ? records : []; |
| | | total.value = Number(data?.total ?? tableData.value.length); |
| | | } catch (e) { |
| | | console.error("å 载工åºå表失败", e); |
| | | tableData.value = []; |
| | | total.value = 0; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | // çå¬å¼¹çªæå¼ï¼éç½®æ¥è¯¢æ¡ä»¶å页ç ï¼immediateï¼ç»ä»¶åå»ºæ¶ modelValue 已为 trueï¼ä¹ç«å³å è½½ï¼ |
| | | watch( |
| | | () => props.modelValue, |
| | | visible => { |
| | | if (visible) { |
| | | selectedRow.value = null; |
| | | query.name = ""; |
| | | query.no = ""; |
| | | page.pageNum = 1; |
| | | loadData(); |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ); |
| | | </script> |
| | |
| | | <el-table-column label="å建æ¶é´" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | {{ parseTime(row.createTime) }} |
| | | {{ parseTime(row.createTime, '{y}-{m}-{d}') }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å·¥æ¶ï¼hï¼" |
| | |
| | | style="width: 160px;" |
| | | @keyup.enter="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="éæ±æ¥æèå´:" |
| | | <el-form-item label="äº¤è´§æ¥æèå´:" |
| | | prop="dateRange"> |
| | | <el-date-picker v-model="searchForm.dateRange" |
| | | type="daterange" |
| | |
| | | <el-input v-model="form.unit" |
| | | placeholder="请è¾å
¥åä½" /> |
| | | </el-form-item> |
| | | <el-form-item label="éæ±æ¥æ" |
| | | <el-form-item label="åè´§æ¥æ" |
| | | prop="requiredDate"> |
| | | <el-date-picker v-model="form.requiredDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 100%" |
| | | placeholder="è¯·éæ©éæ±æ¥æ" /> |
| | | placeholder="è¯·éæ©åè´§æ¥æ" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¿è¯ºæ¥æ" |
| | | prop="promisedDeliveryDate"> |
| | |
| | | // formatData: (cell, row) => (cell ? `${cell}${row.unit || "æ¹"}` : 0), |
| | | }, |
| | | { |
| | | label: "éæ±æ¥æ", |
| | | label: "åè´§æ¥æ", |
| | | prop: "requiredDate", |
| | | width: "160px", |
| | | className: "date-cell", |
| | |
| | | ], |
| | | qtyRequired: [{ required: true, message: "请è¾å
¥æ°é", trigger: "blur" }], |
| | | requiredDate: [ |
| | | { required: true, message: "è¯·éæ©éæ±æ¥æ", trigger: "change" }, |
| | | { required: true, message: "è¯·éæ©åè´§æ¥æ", trigger: "change" }, |
| | | ], |
| | | }); |
| | | |
| | |
| | | }); |
| | | }; |
| | | const onBlur = value => { |
| | | // éå¶åä½å°æ° |
| | | mergeForm.totalAssignedQuantity = Number(value.toFixed(4)); |
| | | mergeForm.totalAssignedQuantity = Number(Number(value).toFixed(10)); |
| | | }; |
| | | |
| | | const fetchProductOptions = () => { |
| | |
| | | }; |
| | | } |
| | | summary[category].totalAssignedQuantity += Number( |
| | | (Number(row.qtyRequired || 0) - Number(row.quantityIssued || 0)).toFixed( |
| | | 4 |
| | | ) |
| | | ( |
| | | Number(row.qtyRequired || 0) - Number(row.quantityIssued || 0) |
| | | ).toFixed(10) |
| | | ); |
| | | }); |
| | | |
| | |
| | | // è®¡ç®æ»å¶é æ°é (é»è®¤qtyRequiredçå) |
| | | const totalAssignedQuantity = selectedRows.value.reduce((sum, row) => { |
| | | return sum + Number(row.qtyRequired || 0) - Number(row.quantityIssued || 0); |
| | | }, 0); |
| | | sumAssignedQuantity.value = totalAssignedQuantity; |
| | | console.log(totalAssignedQuantity); |
| | | }, 0); // é¿å
æµ®ç¹æ°ç²¾åº¦é®é¢ï¼å¦ 37.800000000000004ï¼ |
| | | const fixedTotal = Number(totalAssignedQuantity.toFixed(10)); |
| | | sumAssignedQuantity.value = fixedTotal; |
| | | console.log(fixedTotal); |
| | | // è®¾ç½®è¡¨åæ°æ® |
| | | mergeForm.productName = productName; |
| | | mergeForm.model = firstRow.model || ""; |
| | | mergeForm.totalAssignedQuantity = totalAssignedQuantity; |
| | | mergeForm.totalAssignedQuantity = fixedTotal; |
| | | mergeForm.planCompleteTime = firstRow.requiredDate || ""; |
| | | mergeForm.productId = firstRow.productId || ""; |
| | | mergeForm.ids = selectedRows.value.map(row => row.id); |
| | |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | | <div> |
| | | <span class="search_title">产ååç§°ï¼</span> |
| | | <span class="search_title">éå®åå·ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.salesContractNo" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥éå®åå·æç´¢" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | /> |
| | | <span style="margin-left: 10px" class="search_title">产ååç§°ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.productName" |
| | | style="width: 240px" |
| | |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | salesContractNo: "", |
| | | productName: "", |
| | | entryDate: undefined, // å½å
¥æ¥æ |
| | | entryDateStart: undefined, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog |
| | | v-model="visible" |
| | | title="ä¸åæ ¼å¤ç详æ
" |
| | | width="70%" |
| | | @close="handleClose" |
| | | > |
| | | <el-descriptions border :column="2" class="detail-descriptions"> |
| | | <el-descriptions-item label="ç±»å«"> |
| | | {{ getInspectTypeName(form.inspectType) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="产ååç§°"> |
| | | {{ form.productName }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="è§æ ¼åå·"> |
| | | {{ form.model }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="åä½"> |
| | | {{ form.unit }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="æ°é"> |
| | | {{ form.quantity }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="æ£æµæ¥æ"> |
| | | {{ form.checkTime }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å·¥åº"> |
| | | {{ form.process }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="æ£éªå"> |
| | | {{ form.checkName }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="æ£æµåä½"> |
| | | {{ form.checkCompany }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="ä¸åæ ¼ç°è±¡" :span="2"> |
| | | {{ form.defectivePhenomena }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å¤çç»æ"> |
| | | {{ form.dealResult }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å¤ç人"> |
| | | {{ form.dealName }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å¤çæ¥æ"> |
| | | {{ form.dealTime }} |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="handleClose">å
³é</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from "vue"; |
| | | import { getQualityUnqualifiedDetail } from "@/api/qualityManagement/nonconformingManagement.js"; |
| | | |
| | | const visible = ref(false); |
| | | const form = ref({}); |
| | | |
| | | const getInspectTypeName = (type) => { |
| | | if (type === 0) return "åæææ£éª"; |
| | | if (type === 1) return "è¿ç¨æ£éª"; |
| | | if (type === 2) return "åºåæ£éª"; |
| | | return ""; |
| | | }; |
| | | |
| | | const openDialog = (row) => { |
| | | visible.value = true; |
| | | form.value = { ...row }; |
| | | if (row.id) { |
| | | getQualityUnqualifiedDetail(row.id).then((res) => { |
| | | if (res.data) { |
| | | form.value = { ...res.data }; |
| | | } |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | const handleClose = () => { |
| | | visible.value = false; |
| | | form.value = {}; |
| | | }; |
| | | |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .detail-descriptions :deep(.el-descriptions__label) { |
| | | width: 120px; |
| | | justify-content: flex-end; |
| | | } |
| | | </style> |
| | |
| | | </div> |
| | | <FormDia ref="formDia" @close="handleQuery"></FormDia> |
| | | <InspectionFormDia ref="inspectionFormDia" @close="handleQuery"></InspectionFormDia> |
| | | <DetailModal ref="detailModal"></DetailModal> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | import {ElMessageBox} from "element-plus"; |
| | | import {qualityUnqualifiedDel, qualityUnqualifiedListPage} from "@/api/qualityManagement/nonconformingManagement.js"; |
| | | import InspectionFormDia from "@/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue"; |
| | | import DetailModal from "@/views/qualityManagement/nonconformingManagement/components/detailModal.vue"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | const data = reactive({ |
| | |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 100, |
| | | width: 150, |
| | | operation: [ |
| | | { |
| | | name: "å¤ç", |
| | |
| | | openInspectionForm("edit", row); |
| | | }, |
| | | disabled: (row) => row.inspectState === 1, |
| | | }, |
| | | { |
| | | name: "详æ
", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | detailModal.value?.openDialog(row); |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | |
| | | }); |
| | | const formDia = ref() |
| | | const inspectionFormDia = ref() |
| | | const detailModal = ref() |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | | const changeDaterange = (value) => { |
| | |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | | <div> |
| | | <span class="search_title">å·¥åºï¼</span> |
| | | <span class="search_title">ç产工åå·ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.workOrderNo" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥ç产工åå·æç´¢" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | /> |
| | | <span style="margin-left: 10px" class="search_title">å·¥åºï¼</span> |
| | | <el-input |
| | | v-model="searchForm.process" |
| | | style="width: 240px" |
| | |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | workOrderNo: "", |
| | | process: "", |
| | | entryDate: undefined, // å½å
¥æ¥æ |
| | | entryDateStart: undefined, |
| | |
| | | <div class="app-container"> |
| | | <div class="search_form mb20"> |
| | | <div> |
| | | <span class="search_title">ä¾åºåï¼</span> |
| | | <span class="search_title">éè´è®¢åå·ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.purchaseContractNo" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥éè´è®¢åå·æç´¢" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | /> |
| | | <span style="margin-left: 10px" class="search_title">ä¾åºåï¼</span> |
| | | <el-input |
| | | v-model="searchForm.supplier" |
| | | style="width: 240px" |
| | |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | purchaseContractNo: "", |
| | | supplier: "", |
| | | entryDate: undefined, // å½å
¥æ¥æ |
| | | entryDateStart: undefined, |
| | |
| | | prefix-icon="Search" |
| | | @change="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="åè´§ç¶æï¼"> |
| | | <el-select v-model="searchForm.shippingStatus" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 160px" |
| | | @change="handleQuery"> |
| | | <el-option label="æªåè´§" |
| | | value="æªåè´§" /> |
| | | <el-option label="å·²åè´§" |
| | | value="å·²åè´§" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="å½å
¥æ¥æï¼"> |
| | | <el-date-picker v-model="searchForm.entryDate" |
| | | value-format="YYYY-MM-DD" |
| | |
| | | <!-- åè´§å¼¹æ¡ --> |
| | | <el-dialog v-model="deliveryFormVisible" |
| | | title="åè´§ä¿¡æ¯" |
| | | width="40%" |
| | | width="960px" |
| | | @close="closeDeliveryDia"> |
| | | <el-form :model="deliveryForm" |
| | | label-width="120px" |
| | |
| | | <el-col :span="24"> |
| | | <el-form-item label="å¾
åè´§æ°éï¼"> |
| | | <el-input :model-value="currentDeliveryRow?.noQuantity" |
| | | disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="24"> |
| | | <el-form-item label="已鿩åè´§æ°éï¼"> |
| | | <el-input :model-value="selectedDeliveryQuantity" |
| | | disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | <el-table-column label="æ¹å·" |
| | | prop="batchNo" |
| | | min-width="180" /> |
| | | <el-table-column label="éå®è®¢åå·" |
| | | min-width="180" |
| | | show-overflow-tooltip> |
| | | <template #default="scope"> |
| | | {{ formatDeliveryBatchSalesOrderNo(scope.row) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="产å大类" |
| | | prop="productName" |
| | | min-width="100" /> |
| | |
| | | <el-input-number v-model="scope.row.deliveryQuantity" |
| | | :min="0" |
| | | :max="getDeliveryBatchDeliveryMax(scope.row)" |
| | | :precision="2" |
| | | :step="0.01" |
| | | controls-position="right" |
| | | @change="handleDeliveryBatchQuantityChange(scope.row)" |
| | | style="width: 100%;" /> |
| | |
| | | tableAmountFormatter, |
| | | formatDecimal, |
| | | buildAmountSummaryFormat, |
| | | roundAmount, |
| | | } from "@/utils/numberFormat"; |
| | | import ImageUpload from "@/components/AttachmentUpload/image/index.vue"; |
| | | import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue"; |
| | |
| | | searchForm: { |
| | | customerName: "", // 客æ·åç§° |
| | | salesContractNo: "", // éå®ååç¼å· |
| | | projectName: "", // 项ç®åç§° |
| | | shippingStatus: "", // åè´§ç¶æï¼æªåè´§/å·²åè´§ï¼ |
| | | entryDate: null, // å½å
¥æ¥æ |
| | | entryDateStart: undefined, |
| | | entryDateEnd: undefined, |
| | |
| | | item?.stockQuantity; |
| | | return quantity ?? 0; |
| | | }; |
| | | const formatDeliveryBatchSalesOrderNo = item => |
| | | item?.salesContractNo || "--"; |
| | | const currentStock = ref(null); |
| | | const fetchCurrentStock = async (productModelId) => { |
| | | if (!productModelId) { |
| | |
| | | return; |
| | | } |
| | | try { |
| | | const res = await getStockInventoryByModelId(productModelId); |
| | | const res = await getStockInventoryByModelId(productModelId, currentId.value); |
| | | const rawList = Array.isArray(res?.data) |
| | | ? res.data |
| | | : res?.data?.records || res?.data?.rows || []; |
| | |
| | | rawList.forEach(item => { |
| | | total += Number(getDeliveryBatchQuantity(item) || 0); |
| | | }); |
| | | currentStock.value = total; |
| | | currentStock.value = roundAmount(total); |
| | | } catch (e) { |
| | | console.error(e); |
| | | currentStock.value = 0; |
| | |
| | | const getCurrentDeliveryRowQuantity = () => { |
| | | return Number(currentDeliveryRow.value?.noQuantity || 0); |
| | | }; |
| | | const selectedDeliveryQuantity = computed(() => { |
| | | return roundAmount( |
| | | (deliveryForm.value.batchNoList || []).reduce( |
| | | (sum, item) => sum + Number(item?.deliveryQuantity || 0), |
| | | 0 |
| | | ) |
| | | ); |
| | | }); |
| | | const getDeliveryBatchDeliveryMax = row => { |
| | | const productQuantity = getCurrentDeliveryRowQuantity(); |
| | | const batchQuantity = Number(getDeliveryBatchQuantity(row) || 0); |
| | | const otherBatchTotal = (deliveryForm.value.batchNoList || []).reduce( |
| | | const otherBatchTotal = roundAmount( |
| | | (deliveryForm.value.batchNoList || []).reduce( |
| | | (sum, item) => { |
| | | if (item?.id === row?.id) return sum; |
| | | return sum + Number(item?.deliveryQuantity || 0); |
| | | }, |
| | | 0 |
| | | ) |
| | | ); |
| | | const remainingProductQuantity = Math.max( |
| | | 0, |
| | | productQuantity - otherBatchTotal |
| | | const remainingProductQuantity = roundAmount( |
| | | Math.max(0, productQuantity - otherBatchTotal) |
| | | ); |
| | | return Math.max(0, Math.min(batchQuantity, remainingProductQuantity)); |
| | | }; |
| | | const handleDeliveryBatchQuantityChange = row => { |
| | | const productQuantity = getCurrentDeliveryRowQuantity(); |
| | | const batchQuantity = Number(getDeliveryBatchQuantity(row) || 0); |
| | | const otherBatchTotal = (deliveryForm.value.batchNoList || []).reduce( |
| | | const otherBatchTotal = roundAmount( |
| | | (deliveryForm.value.batchNoList || []).reduce( |
| | | (sum, item) => { |
| | | if (item?.id === row?.id) return sum; |
| | | return sum + Number(item?.deliveryQuantity || 0); |
| | | }, |
| | | 0 |
| | | ) |
| | | ); |
| | | const remainingProductQuantity = Math.max( |
| | | 0, |
| | | productQuantity - otherBatchTotal |
| | | const remainingProductQuantity = roundAmount( |
| | | Math.max(0, productQuantity - otherBatchTotal) |
| | | ); |
| | | const currentValue = Number(row?.deliveryQuantity || 0); |
| | | |
| | |
| | | item => Number(item?.deliveryQuantity || 0) > 0 |
| | | ); |
| | | }; |
| | | const getDeliveryBatchNoList = async productModelId => { |
| | | const getDeliveryBatchNoList = async (productModelId, salesLedgerId) => { |
| | | if (!productModelId) return []; |
| | | const res = await getStockInventoryByModelId(productModelId); |
| | | const res = await getStockInventoryByModelId(productModelId, salesLedgerId); |
| | | const rawList = Array.isArray(res?.data) |
| | | ? res.data |
| | | : res?.data?.records || res?.data?.rows || []; |
| | |
| | | |
| | | currentDeliveryRow.value = row; |
| | | const batchNoList = await getDeliveryBatchNoList( |
| | | row.productModelId || row.modelId |
| | | row.productModelId || row.modelId, |
| | | row.salesLedgerId |
| | | ); |
| | | deliveryForm.value = { |
| | | shippingCarNumber: "", |
| | |
| | | proxy.$modal.msgWarning("请è³å°å¡«åä¸ä¸ªæ¹å·çåè´§æ°é"); |
| | | return; |
| | | } |
| | | const totalDeliveryQuantity = selectedBatchRows.reduce( |
| | | const totalDeliveryQuantity = roundAmount( |
| | | selectedBatchRows.reduce( |
| | | (sum, item) => sum + Number(item.deliveryQuantity || 0), |
| | | 0 |
| | | ) |
| | | ); |
| | | const currentRowNoQuantity = Number( |
| | | currentDeliveryRow.value?.noQuantity || 0 |