src/api/basicData/customerFile.js
@@ -9,6 +9,41 @@ params: query }) } // æ¥è¯¢å°åºå表 export function listCustomerRegions(query) { return request({ url: '/customerRegions/list', method: 'get', params: query }) } // æ°å¢å°åº export function addCustomerRegions(data) { return request({ url: '/customerRegions/add', method: 'post', data }) } // ä¿®æ¹å°åº export function updateCustomerRegions(data) { return request({ url: '/customerRegions/update', method: 'put', data }) } // å é¤å°åº export function delCustomerRegions(id) { return request({ url: '/customerRegions/' + id, method: 'delete' }) } // æ¥è¯¢å®¢æ·æ¡£æ¡è¯¦ç» export function getCustomer(id) { return request({ src/api/salesManagement/salesLedger.js
@@ -151,4 +151,56 @@ url: `/salesLedgerProductProcess/delete/${id}`, method: "delete", }); } // éå®å°è´¦-ç»å®å·¥èºè·¯çº¿ export function saleProcessBind(data) { return request({ url: "/sales/ledger/saleProcessBind", method: "post", data, }); } // éå®å°è´¦-æ¥è¯¢è®¢åå·²ç»å®å·¥èºè·¯çº¿ export function getSaleProcessBindInfo(salesLedgerId) { return request({ url: `/sales/ledger/salesProcess/${salesLedgerId}`, method: "get", }); } // æå°ç产æµç¨å¡ï¼æåï¼ export function getProcessCard(salesLedgerId) { return request({ url: `/sales/ledger/processCard/${salesLedgerId}`, method: "get", }); } // æå°éå®è®¢å export function getSalesOrder(salesLedgerId) { return request({ url: `/sales/ledger/salesOrders/${salesLedgerId}`, method: "get", }); } // æå°éå®åè´§å export function getSalesInvoices(query) { const data = query && typeof query === "object" ? query : { salesLedgerId: query }; return request({ url: "/sales/ledger/salesInvoices", method: "post", data, }); } // æå°é宿 ç¾ export function getSalesLabel(salesLedgerId) { return request({ url: `/sales/ledger/salesLabel/${salesLedgerId}`, method: "get", }); } src/api/salesManagement/salesProcessFlowConfig.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,89 @@ // å·¥èºæµç¨é ç½®ï¼æ¨¡æ¿ï¼ä¸ç»å®å°éå®å°è´¦äº§åçæ¥å£ import request from "@/utils/request"; // å·¥èºè·¯çº¿å页å表 export function salesProcessFlowConfigList(query = {}) { return request({ url: "/processRoute/page", method: "get", params: query, }); } // å·¥èºè·¯çº¿è¯¦æ ï¼å ¼å®¹æ§è°ç¨ï¼ export function salesProcessFlowConfigGetById(configId) { return request({ url: `/processRoute/${configId}`, method: "get", }); } // æ°å¢/ç¼è¾å·¥èºè·¯çº¿ export function salesProcessFlowConfigUpsert(data) { return request({ url: "/processRoute", method: "post", data: data, }); } // å é¤å·¥èºè·¯çº¿ export function salesProcessFlowConfigDelete(configId) { return request({ url: `/processRoute/${configId}`, method: "delete", }); } // 设置é»è®¤å·¥èºè·¯çº¿ export function salesProcessFlowConfigSetDefault(configId) { return request({ url: `/processRoute/default/${configId}`, method: "put", }); } // æ¥è¯¢å·¥èºè·¯çº¿ä¸çå·¥åº export function salesProcessFlowConfigItemList(routeId) { return request({ url: "/processRouteItem/list", method: "get", params: { routeId }, }); } // æ°å¢/ä¿®æ¹å·¥åº export function salesProcessFlowConfigItemUpsert(data) { return request({ url: "/processRouteItem/", method: "post", data, }); } // å·¥åºæåºæ¥å£ export function salesProcessFlowConfigItemSort(data) { return request({ url: "/processRouteItem/sort", method: "post", data, }); } // å é¤å·¥åº export function salesProcessFlowConfigItemDelete(itemId) { return request({ url: `/processRouteItem/batchDelete/${itemId}`, method: "delete", }); } // å°æå¥é ç½®åºç¨å°å ·ä½äº§åï¼åæ¥æ´æ°ï¼ export function salesLedgerProductSetProcessFlowConfig(data) { return request({ url: "/salesLedgerProduct/setProcessFlowConfig", method: "post", data: data, }); } src/views/basicData/customerFile/index.vue
@@ -1,52 +1,158 @@ <template> <div class="app-container"> <div class="search_form"> <div> <span class="search_title">客æ·åç§°ï¼</span> <el-input v-model="searchForm.customerName" style="width: 240px;margin-right: 10px" placeholder="请è¾å ¥" @change="handleQuery" clearable :prefix-icon="Search" /> <span class="search_title">客æ·åç±»ï¼</span> <el-select v-model="searchForm.customerType" placeholder="è¯·éæ©" style="width: 240px" clearable @change="handleQuery"> <el-option label="é¶å®å®¢æ·" value="é¶å®å®¢æ·" /> <el-option label="è¿éå客æ·" value="è¿éå客æ·" /> </el-select> <el-button type="primary" @click="handleQuery" style="margin-left: 10px">æç´¢</el-button> <div class="customer-split"> <div class="left-panel"> <div class="left-header"> <div class="left-title">å°åº</div> <div class="left-actions"> <el-button type="primary" size="small" @click="openAddRegionDialog">æ°å¢</el-button> <el-button size="small" @click="openEditRegionDialog">ä¿®æ¹</el-button> <el-button type="danger" plain size="small" @click="handleDeleteRegion">å é¤</el-button> </div> </div> <div class="left-search"> <el-input v-model="regionKeyword" placeholder="æ¥è¯¢å°åº" clearable :prefix-icon="Search" /> </div> <div class="left-list"> <el-skeleton v-if="regionsLoading" :rows="8" animated /> <template v-else> <div class="region-item" :class="{ active: selectedRegionId === 0 }" @click="selectRegion('')">å ¨é¨</div> <el-tree ref="regionTreeRef" :data="regionTreeData" node-key="id" :props="regionTreeProps" :filter-node-method="filterRegionNode" highlight-current default-expand-all :expand-on-click-node="false" @node-click="handleRegionNodeClick" /> <div v-if="regionTreeData.length === 0" class="empty-tip">ææ å°åº</div> </template> </div> </div> <div> <el-button type="primary" @click="openForm('add')">æ°å¢å®¢æ·</el-button> <el-button @click="handleOut">导åº</el-button> <el-button type="info" plain icon="Upload" @click="handleImport">å¯¼å ¥</el-button> <el-button type="danger" plain @click="handleDelete">å é¤</el-button> <div class="right-panel"> <div class="toolbar-card"> <div class="search_form right-search-form"> <div class="search-fields"> <span class="search_title">客æ·åç§°ï¼</span> <el-input v-model="searchForm.customerName" style="width: 240px;margin-right: 10px" placeholder="请è¾å ¥" @change="handleQuery" clearable :prefix-icon="Search" /> <span class="search_title">客æ·åç±»ï¼</span> <el-select v-model="searchForm.customerType" placeholder="è¯·éæ©" style="width: 240px" clearable @change="handleQuery"> <el-option label="é¶å®å®¢æ·" value="é¶å®å®¢æ·" /> <el-option label="è¿éå客æ·" value="è¿éå客æ·" /> </el-select> <el-button type="primary" @click="handleQuery" style="margin-left: 10px">æç´¢</el-button> </div> <div class="toolbar-divider"></div> <div class="action-buttons"> <el-button type="primary" @click="openForm('add')">æ°å¢å®¢æ·</el-button> <el-button @click="handleOut">导åº</el-button> <el-button type="info" plain icon="Upload" @click="handleImport">å¯¼å ¥</el-button> <el-button type="danger" plain @click="handleDelete">å é¤</el-button> </div> </div> </div> <div class="table_list table-card"> <PIMTable rowKey="id" :column="tableColumn" :tableData="tableData" :page="page" :isSelection="true" @selection-change="handleSelectionChange" :tableLoading="tableLoading" @pagination="pagination"></PIMTable> </div> </div> </div> <div class="table_list"> <PIMTable rowKey="id" :column="tableColumn" :tableData="tableData" :page="page" :isSelection="true" @selection-change="handleSelectionChange" :tableLoading="tableLoading" @pagination="pagination"></PIMTable> </div> <el-dialog v-model="addRegionDialogVisible" title="æ°å¢å°åº" width="420px" @close="closeAddRegionDialog"> <el-form :model="addRegionForm" label-width="90px"> <el-form-item label="ä¸çº§å°åº"> <el-cascader v-model="addRegionForm.parentPath" :options="regionTreeData" :props="regionCascaderProps" clearable filterable placeholder="ä¸éå为顶级å°åº" /> </el-form-item> <el-form-item label="å°åºåç§°"> <el-input v-model="addRegionForm.regionsName" placeholder="请è¾å ¥" clearable /> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="submitAddRegion">确认</el-button> <el-button @click="closeAddRegionDialog">åæ¶</el-button> </div> </template> </el-dialog> <el-dialog v-model="editRegionDialogVisible" title="ä¿®æ¹å°åº" width="420px" @close="closeEditRegionDialog"> <el-form :model="editRegionForm" label-width="90px"> <el-form-item label="ä¸çº§å°åº"> <el-input :value="editRegionParentLabel" disabled /> </el-form-item> <el-form-item label="å°åºåç§°"> <el-input v-model="editRegionForm.regionsName" placeholder="请è¾å ¥" clearable /> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="submitEditRegion">确认</el-button> <el-button @click="closeEditRegionDialog">åæ¶</el-button> </div> </template> </el-dialog> <el-dialog v-model="dialogFormVisible" :title="operationType === 'add' ? 'æ°å¢å®¢æ·ä¿¡æ¯' : 'ç¼è¾å®¢æ·ä¿¡æ¯'" width="70%" @@ -76,19 +182,47 @@ </el-row> <el-row :gutter="30"> <el-col :span="12"> <el-form-item label="å ¬å¸å°åï¼" <el-form-item label="客æ·å°åºï¼" prop="regions"> <el-cascader v-model="formRegionPath" :options="regionTreeData" :props="regionCascaderProps" clearable filterable style="width: 100%" placeholder="è¯·éæ©" @change="handleFormRegionChange" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="客æ·å°åï¼" prop="companyAddress"> <el-input v-model="form.companyAddress" placeholder="请è¾å ¥" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="30"> <el-col :span="12"> <el-form-item label="å ¬å¸çµè¯ï¼" <el-form-item label="客æ·çµè¯ï¼" prop="companyPhone"> <el-input v-model="form.companyPhone" placeholder="请è¾å ¥" clearable /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="客æ·åç±»ï¼" prop="customerType"> <el-select v-model="form.customerType" placeholder="è¯·éæ©" clearable> <el-option label="é¶å®å®¢æ·" value="é¶å®å®¢æ·" /> <el-option label="è¿éå客æ·" value="è¿éå客æ·" /> </el-select> </el-form-item> </el-col> </el-row> @@ -117,19 +251,6 @@ <el-input v-model="form.bankCode" placeholder="请è¾å ¥" clearable /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="客æ·åç±»ï¼" prop="customerType"> <el-select v-model="form.customerType" placeholder="è¯·éæ©" clearable> <el-option label="é¶å®å®¢æ·" value="é¶å®å®¢æ·" /> <el-option label="è¿éå客æ·" value="è¿éå客æ·" /> </el-select> </el-form-item> </el-col> </el-row> @@ -240,130 +361,10 @@ </div> </template> </el-dialog> <!-- å访æéå¯¹è¯æ¡ --> <el-dialog title="å访æé" v-model="reminderDialogVisible" width="500px" @close="closeReminderDialog"> <el-form :model="reminderForm" label-width="100px" :rules="reminderRules" ref="reminderFormRef"> <el-form-item label="客æ·åç§°ï¼"> <el-input v-model="reminderForm.customerName" disabled /> </el-form-item> <el-form-item label="æéå¼å ³ï¼"> <el-switch v-model="reminderForm.reminderSwitch" /> </el-form-item> <el-form-item label="æéå 容ï¼" prop="reminderContent"> <el-input v-model="reminderForm.reminderContent" type="textarea" :maxlength="100" show-word-limit placeholder="请è¾å ¥æéå 容" /> </el-form-item> <el-form-item label="æéæ¶é´ï¼" prop="reminderTime"> <el-date-picker v-model="reminderForm.reminderTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss" placeholder="è¯·éæ©æéæ¶é´" style="width: 100%" /> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="submitReminderForm">确认</el-button> <el-button @click="closeReminderDialog">åæ¶</el-button> </div> </template> </el-dialog> <!-- æ·»å /ä¿®æ¹æ´½è°è¿åº¦å¯¹è¯æ¡ --> <el-dialog :title="negotiationForm.editIndex !== undefined ? 'ä¿®æ¹è¿åº¦' : 'æ·»å è¿åº¦'" v-model="negotiationDialogVisible" width="600px" @close="closeNegotiationDialog"> <el-form :model="negotiationForm" label-width="100px" :rules="negotiationRules" ref="negotiationFormRef"> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="è·è¿æ¹å¼ï¼" prop="followUpMethod"> <el-select v-model="negotiationForm.followUpMethod" placeholder="è¯·éæ©" style="width: 100%"> <el-option label="çµè¯" value="çµè¯" /> <el-option label="é®ä»¶" value="é®ä»¶" /> <el-option label="ä¸é¨" value="ä¸é¨" /> <el-option label="微信" value="微信" /> <el-option label="å ¶ä»" value="å ¶ä»" /> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="è·è¿ç¨åº¦ï¼" prop="followUpLevel"> <el-select v-model="negotiationForm.followUpLevel" placeholder="è¯·éæ©" style="width: 100%"> <el-option label="æ½å¨å®¢æ·" value="æ½å¨å®¢æ·" /> <el-option label="忬¡æè®¿" value="忬¡æè®¿" /> <el-option label="夿¬¡æè®¿" value="夿¬¡æè®¿" /> <el-option label="æå客æ·" value="æå客æ·" /> <el-option label="å·²ç¾çº¦å®¢æ·" value="å·²ç¾çº¦å®¢æ·" /> </el-select> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="è·è¿æ¶é´ï¼" prop="followUpTime"> <el-date-picker v-model="negotiationForm.followUpTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss" placeholder="è¯·éæ©" style="width: 100%" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="è·è¿äººï¼"> <el-input v-model="negotiationForm.followerUserName" disabled /> </el-form-item> </el-col> </el-row> <el-form-item label="å 容ï¼" prop="content"> <el-input v-model="negotiationForm.content" type="textarea" :rows="4" placeholder="请è¾å ¥" /> </el-form-item> </el-form> <template #footer> <div class="dialog-footer"> <el-button type="primary" @click="submitNegotiationForm">确认</el-button> <el-button @click="closeNegotiationDialog">åæ¶</el-button> </div> </template> </el-dialog> <!-- å访æé / æ´½è°è¿åº¦åè½ï¼å ¥å£åå¼¹çªï¼å·²æéæ±æ³¨éã å¦éæ¢å¤ï¼æ¾å¼æä½åå ¥å£ + åæ¶æ¬æ®µæ³¨é + æ¢å¤ script ä¸ç¸å ³åé/æ¹æ³/æ¥å£å¼å ¥ã --> <!-- 客æ·è¯¦æ å¯¹è¯æ¡ --> <el-dialog title="客æ·è¯¦æ " v-model="detailDialogVisible" @@ -396,7 +397,7 @@ </el-col> <el-col :span="12"> <div class="info-item"> <span class="info-label">å ¬å¸çµè¯ï¼</span> <span class="info-label">客æ·çµè¯ï¼</span> <span class="info-value">{{ detailForm.companyPhone }}</span> </div> </el-col> @@ -404,52 +405,60 @@ <el-row :gutter="20"> <el-col :span="12"> <div class="info-item"> <span class="info-label">å ¬å¸å°åï¼</span> <span class="info-label">客æ·å°åºï¼</span> <span class="info-value">{{ detailForm.regionsName || detailForm.regions }}</span> </div> </el-col> <el-col :span="12"> <div class="info-item"> <span class="info-label">客æ·å°åï¼</span> <span class="info-value">{{ detailForm.companyAddress }}</span> </div> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <div class="info-item"> <span class="info-label">é¶è¡åºæ¬æ·ï¼</span> <span class="info-value">{{ detailForm.basicBankAccount }}</span> </div> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <div class="info-item"> <span class="info-label">é¶è¡è´¦å·ï¼</span> <span class="info-value">{{ detailForm.bankAccount }}</span> </div> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <div class="info-item"> <span class="info-label">弿·è¡å·ï¼</span> <span class="info-value">{{ detailForm.bankCode }}</span> </div> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <div class="info-item"> <span class="info-label">è系人ï¼</span> <span class="info-value">{{ detailForm.contactPerson }}</span> </div> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <div class="info-item"> <span class="info-label">èç³»çµè¯ï¼</span> <span class="info-value">{{ detailForm.contactPhone }}</span> </div> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <div class="info-item"> <span class="info-label">ç»´æ¤äººï¼</span> <span class="info-value">{{ detailForm.maintainer }}</span> </div> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <div class="info-item"> <span class="info-label">ç»´æ¤æ¶é´ï¼</span> @@ -459,169 +468,38 @@ </el-row> </div> </div> <!-- æ´½è°è¿åº¦è®°å½ --> <div class="detail-section"> <div class="section-header"> <h3 class="section-title">æ´½è°è¿åº¦è®°å½</h3> <el-button type="primary" size="small" @click="openNegotiationDialog(detailForm)"> æ·»å è¿åº¦ </el-button> </div> <el-table :data="negotiationRecords" border style="width: 100%"> <el-table-column prop="followUpTime" label="è·è¿æ¶é´" width="160" /> <el-table-column prop="followUpMethod" label="è·è¿æ¹å¼" width="100" /> <el-table-column prop="followUpLevel" label="è·è¿ç¨åº¦" /> <el-table-column prop="followerUserName" label="è·è¿äºº" width="100" /> <el-table-column prop="content" label="å 容" show-overflow-tooltip /> <el-table-column label="éä»¶" width="100" align="center"> <template #default="{ row }"> <el-button type="info" link @click="openAttachmentDialog(row)"> <el-icon> <Paperclip /> </el-icon> éä»¶ <!-- {{ row.fileList && row.fileList.length > 0 ? row.fileList.length : 'ä¸ä¼ ' }} --> </el-button> </template> </el-table-column> <el-table-column label="æä½" width="150" align="center"> <template #default="{ row, $index }"> <el-button type="primary" link @click="editNegotiationRecord(row, $index)"> ä¿®æ¹ </el-button> <el-button type="danger" link @click="deleteNegotiationRecord(row, $index)"> å é¤ </el-button> </template> </el-table-column> </el-table> <div v-if="negotiationRecords.length === 0" class="no-records"> ææ æ´½è°è¿åº¦è®°å½ </div> </div> <!-- æ´½è°è¿åº¦è®°å½ï¼å«éä»¶/ä¿®æ¹/å é¤ï¼å·²æéæ±æ´ä½æ³¨éã å¦éæ¢å¤ï¼åæ¶æ¬æ®µæ³¨éï¼å¹¶æ¢å¤ script ä¸ç¸å ³åé/æ¹æ³/æ¥å£å¼å ¥ã --> <template #footer> <div class="dialog-footer"> <el-button @click="closeDetailDialog">å ³é</el-button> </div> </template> </el-dialog> <!-- éä»¶ä¸ä¼ å¼¹çª --> <el-dialog title="é件管ç" v-model="attachmentDialogVisible" width="600px" @close="closeAttachmentDialog"> <div class="attachment-section"> <div class="upload-area"> <el-upload ref="attachmentUploadRef" :action="getAttachmentUploadUrl()" :headers="attachmentUploadHeaders" :file-list="currentAttachmentList" :on-success="handleAttachmentSuccess" :on-error="handleAttachmentError" :on-remove="handleAttachmentRemove" :before-upload="beforeAttachmentUpload" multiple :limit="10" name="files"> <el-button type="primary"> <el-icon> <Upload /> </el-icon> ä¸ä¼ éä»¶ </el-button> <template #tip> <div class="el-upload__tip"> æ¯æä¸ä¼ å¾çãææ¡£çæä»¶ï¼å个æä»¶ä¸è¶ è¿50MB </div> </template> </el-upload> </div> <div v-if="currentAttachmentList.length > 0" class="attachment-list"> <h4>å·²ä¸ä¼ éä»¶ï¼</h4> <el-table :data="currentAttachmentList" border size="small"> <el-table-column prop="name" label="æä»¶å" show-overflow-tooltip /> <el-table-column prop="size" label="大å°" width="100"> <template #default="{ row }"> {{ formatFileSize(row.size) }} </template> </el-table-column> <el-table-column label="æä½" width="120" align="center"> <template #default="{ row, $index }"> <el-button type="primary" link @click="downloadAttachment(row)"> ä¸è½½ </el-button> <el-button type="danger" link @click="deleteAttachment(row, $index)"> å é¤ </el-button> </template> </el-table-column> </el-table> </div> <div v-else class="no-attachment"> ææ éä»¶ </div> </div> <template #footer> <div class="dialog-footer"> <el-button @click="closeAttachmentDialog">å ³é</el-button> </div> </template> </el-dialog> <!-- é件管çåè½å·²éæ´½è°è¿åº¦æ´ä½æ³¨é --> </div> </template> <script setup> import { onMounted, ref, reactive, getCurrentInstance, toRefs } from "vue"; import { Search, Paperclip, Upload } from "@element-plus/icons-vue"; import { onMounted, ref, reactive, getCurrentInstance, toRefs, computed, watch } from "vue"; import { Search } from "@element-plus/icons-vue"; import { addCustomer, delCustomer, getCustomer, listCustomer, listCustomerRegions, addCustomerRegions, updateCustomerRegions, delCustomerRegions, updateCustomer, addCustomerFollow, updateCustomerFollow, delCustomerFollow, addReturnVisit, getReturnVisit, // addCustomerFollow, // updateCustomerFollow, // delCustomerFollow, // addReturnVisit, // getReturnVisit, } from "@/api/basicData/customerFile.js"; import { ElMessageBox } from "element-plus"; import { userListNoPage } from "@/api/system/user.js"; @@ -630,54 +508,55 @@ const { proxy } = getCurrentInstance(); const userStore = useUserStore(); // å访æéç¸å ³ const reminderDialogVisible = ref(false); const reminderFormRef = ref(); const currentCustomerId = ref(); const reminderForm = reactive({ customerName: "", reminderSwitch: false, reminderContent: "", reminderTime: "", }); const reminderRules = { reminderContent: [ { required: true, message: "请è¾å ¥æéå 容", trigger: "blur" }, ], reminderTime: [ { required: true, message: "è¯·éæ©æéæ¶é´", trigger: "change" }, ], }; // æ´½è°è¿åº¦ç¸å ³ const negotiationDialogVisible = ref(false); const negotiationFormRef = ref(); const negotiationForm = reactive({ customerName: "", customerId: "", followUpMethod: "", followUpLevel: "", followUpTime: "", followerUserName: "", content: "", }); const negotiationRules = { followUpMethod: [ { required: true, message: "è¯·éæ©è·è¿æ¹å¼", trigger: "change" }, ], followUpLevel: [ { required: true, message: "è¯·éæ©è·è¿ç¨åº¦", trigger: "change" }, ], followUpTime: [ { required: true, message: "è¯·éæ©è·è¿æ¶é´", trigger: "change" }, ], content: [{ required: true, message: "请è¾å ¥å 容", trigger: "blur" }], }; // å访æé/æ´½è°è¿åº¦ç¸å ³ï¼å·²æéæ±æ´ä½æ³¨éï¼ // const reminderDialogVisible = ref(false); // const reminderFormRef = ref(); // const currentCustomerId = ref(); // const reminderForm = reactive({ // customerName: "", // reminderSwitch: false, // reminderContent: "", // reminderTime: "", // }); // const reminderRules = { // reminderContent: [ // { required: true, message: "请è¾å ¥æéå 容", trigger: "blur" }, // ], // reminderTime: [ // { required: true, message: "è¯·éæ©æéæ¶é´", trigger: "change" }, // ], // }; // // const negotiationDialogVisible = ref(false); // const negotiationFormRef = ref(); // const negotiationForm = reactive({ // customerName: "", // customerId: "", // followUpMethod: "", // followUpLevel: "", // followUpTime: "", // followerUserName: "", // content: "", // }); // const negotiationRules = { // followUpMethod: [ // { required: true, message: "è¯·éæ©è·è¿æ¹å¼", trigger: "change" }, // ], // followUpLevel: [ // { required: true, message: "è¯·éæ©è·è¿ç¨åº¦", trigger: "change" }, // ], // followUpTime: [ // { required: true, message: "è¯·éæ©è·è¿æ¶é´", trigger: "change" }, // ], // content: [{ required: true, message: "请è¾å ¥å 容", trigger: "blur" }], // }; // 详æ ç¸å ³ const detailDialogVisible = ref(false); const detailForm = reactive({ customerName: "", regionsName: "", regions: "", customerType: "", taxpayerIdentificationNumber: "", companyPhone: "", @@ -692,21 +571,13 @@ }); const negotiationRecords = ref([]); // éä»¶ç¸å ³ const attachmentDialogVisible = ref(false); const attachmentUploadRef = ref(); const currentAttachmentList = ref([]); const currentFollowRecord = ref({}); const attachmentUploadHeaders = { Authorization: "Bearer " + getToken() }; // 卿æå»ºä¸ä¼ URL const getAttachmentUploadUrl = () => { const baseUrl = import.meta.env.VITE_APP_BASE_API + "/basic/customer-follow/upload"; return currentFollowRecord.value.id ? `${baseUrl}/${currentFollowRecord.value.id}` : baseUrl; }; // éä»¶ç¸å ³ï¼éæ´½è°è¿åº¦æ´ä½æ³¨éï¼ // const attachmentDialogVisible = ref(false); // const attachmentUploadRef = ref(); // const currentAttachmentList = ref([]); // const currentFollowRecord = ref({}); // const attachmentUploadHeaders = { Authorization: "Bearer " + getToken() }; // const getAttachmentUploadUrl = () => {}; const tableColumn = ref([ { @@ -718,6 +589,11 @@ label: "客æ·åç§°", prop: "customerName", width: 220, }, { label: "客æ·å°åº", prop: "regionsName", width: 120, }, { label: "纳ç¨äººè¯å«ç ", @@ -777,7 +653,7 @@ label: "æä½", align: "center", fixed: "right", width: 290, width: 120, operation: [ { name: "ç¼è¾", @@ -786,20 +662,20 @@ openForm("edit", row); }, }, { name: "æ·»å æ´½è°è¿åº¦", type: "text", clickFun: row => { openNegotiationDialog(row); }, }, { name: "å访æé", type: "text", clickFun: row => { openReminderDialog(row); }, }, // { // name: "æ·»å æ´½è°è¿åº¦", // type: "text", // clickFun: row => { // openNegotiationDialog(row); // }, // }, // { // name: "å访æé", // type: "text", // clickFun: row => { // openReminderDialog(row); // }, // }, { name: "详æ ", type: "text", @@ -837,11 +713,16 @@ searchForm: { customerName: "", customerType: "", regions: "", regionsId: "", }, form: { customerName: "", taxpayerIdentificationNumber: "", companyAddress: "", regions: "", regionsId: "", regionsld: "", companyPhone: "", contactPerson: "", contactPhone: "", @@ -858,6 +739,7 @@ { required: true, message: "请è¾å ¥", trigger: "blur" }, ], companyAddress: [{ required: true, message: "请è¾å ¥", trigger: "blur" }], regions: [{ required: true, message: "è¯·éæ©å®¢æ·å°åº", trigger: "change" }], companyPhone: [{ required: true, message: "请è¾å ¥", trigger: "blur" }], // contactPerson: [{ required: true, message: "请è¾å ¥", trigger: "blur" }], // contactPhone: [{ required: true, message: "请è¾å ¥", trigger: "blur" }], @@ -927,6 +809,189 @@ }, }); const { searchForm, form, rules } = toRefs(data); // 左侧å°åºæ const regionTreeRef = ref(); const regionsLoading = ref(false); const regionTreeData = ref([]); const regionKeyword = ref(""); const selectedRegionId = ref(0); // 0 è¡¨ç¤ºå ¨é¨ const selectedRegionNode = ref(null); const formRegionPath = ref([]); const addRegionDialogVisible = ref(false); const editRegionDialogVisible = ref(false); const addRegionForm = reactive({ parentPath: [], regionsName: "" }); const editRegionForm = reactive({ id: undefined, parentId: 0, regionsName: "" }); const regionTreeProps = { label: "label", children: "children" }; const regionCascaderProps = { value: "id", label: "label", children: "children", checkStrictly: true, emitPath: true, }; const regionNodeMap = computed(() => { const map = new Map(); const walk = list => { (list || []).forEach(node => { map.set(node.id, node); walk(node.children || []); }); }; walk(regionTreeData.value); return map; }); const editRegionParentLabel = computed(() => { if (!editRegionForm.parentId) return "顶级å°åº"; return regionNodeMap.value.get(editRegionForm.parentId)?.label || "æªç¥"; }); const normalizeRegionTree = list => { return (list || []).map(item => ({ ...item, label: item.label || item.regionsName || "", regionsName: item.regionsName || item.label || "", children: normalizeRegionTree(item.children || []), })); }; const fetchRegions = async () => { regionsLoading.value = true; try { const res = await listCustomerRegions({}); const list = res?.data ?? res?.rows ?? res ?? []; regionTreeData.value = Array.isArray(list) ? normalizeRegionTree(list) : []; } catch (e) { console.error("å°åºæ¥è¯¢å¤±è´¥:", e); regionTreeData.value = []; } finally { regionsLoading.value = false; } }; const filterRegionNode = (value, data) => { if (!value) return true; return (data.label || "").includes(value); }; const selectRegion = regionName => { selectedRegionId.value = 0; selectedRegionNode.value = null; searchForm.value.regions = regionName || ""; searchForm.value.regionsId = ""; handleQuery(); }; const handleRegionNodeClick = data => { selectedRegionId.value = data.id; selectedRegionNode.value = data; searchForm.value.regions = data.regionsName || data.label || ""; searchForm.value.regionsId = data.id; handleQuery(); }; const openAddRegionDialog = () => { addRegionForm.parentPath = []; addRegionForm.regionsName = ""; addRegionDialogVisible.value = true; }; const closeAddRegionDialog = () => { addRegionDialogVisible.value = false; addRegionForm.parentPath = []; addRegionForm.regionsName = ""; }; const submitAddRegion = async () => { const name = (addRegionForm.regionsName || "").trim(); if (!name) return proxy.$modal.msgWarning("请è¾å ¥å°åºåç§°"); const parentPath = addRegionForm.parentPath || []; const parentId = parentPath.length ? parentPath[parentPath.length - 1] : 0; await addCustomerRegions({ parentId, regionsName: name }); proxy.$modal.msgSuccess("æ°å¢æå"); await fetchRegions(); closeAddRegionDialog(); }; const openEditRegionDialog = () => { if (!selectedRegionNode.value || selectedRegionId.value === 0) { return proxy.$modal.msgWarning("请å éæ©è¦ä¿®æ¹çå°åº"); } editRegionForm.id = selectedRegionNode.value.id; editRegionForm.parentId = selectedRegionNode.value.parentId || 0; editRegionForm.regionsName = selectedRegionNode.value.regionsName || selectedRegionNode.value.label || ""; editRegionDialogVisible.value = true; }; const closeEditRegionDialog = () => { editRegionDialogVisible.value = false; editRegionForm.id = undefined; editRegionForm.parentId = 0; editRegionForm.regionsName = ""; }; const submitEditRegion = async () => { const name = (editRegionForm.regionsName || "").trim(); if (!name) return proxy.$modal.msgWarning("请è¾å ¥å°åºåç§°"); await updateCustomerRegions({ id: editRegionForm.id, parentId: editRegionForm.parentId, regionsName: name, }); proxy.$modal.msgSuccess("ä¿®æ¹æå"); await fetchRegions(); closeEditRegionDialog(); }; const handleDeleteRegion = () => { if (!selectedRegionNode.value || selectedRegionId.value === 0) { return proxy.$modal.msgWarning("请å éæ©è¦å é¤çå°åº"); } ElMessageBox.confirm("å é¤åä¸å¯æ¢å¤ï¼æ¯å¦ç»§ç»ï¼", "å é¤å°åº", { confirmButtonText: "确认", cancelButtonText: "åæ¶", type: "warning", }) .then(async () => { await delCustomerRegions(selectedRegionNode.value.id); proxy.$modal.msgSuccess("å 餿å"); selectRegion(""); await fetchRegions(); }) .catch(() => {}); }; const handleFormRegionChange = value => { const ids = value || []; if (!ids.length) { form.value.regions = ""; form.value.regionsId = ""; form.value.regionsld = ""; return; } const lastId = ids[ids.length - 1]; form.value.regions = regionNodeMap.value.get(lastId)?.regionsName || ""; form.value.regionsId = lastId; form.value.regionsld = lastId; }; const findRegionPathByName = (tree, targetName, parentPath = []) => { for (const item of tree || []) { const currentPath = [...parentPath, item.id]; if ((item.regionsName || item.label) === targetName) { return currentPath; } const childResult = findRegionPathByName(item.children || [], targetName, currentPath); if (childResult.length) return childResult; } return []; }; const findRegionPathById = (tree, targetId, parentPath = []) => { if (targetId === undefined || targetId === null || targetId === "") return []; for (const item of tree || []) { const currentPath = [...parentPath, item.id]; if (String(item.id) === String(targetId)) { return currentPath; } const childResult = findRegionPathById(item.children || [], targetId, currentPath); if (childResult.length) return childResult; } return []; }; const addNewContact = () => { formYYs.value.contactList.push({ contactPerson: "", @@ -980,6 +1045,9 @@ const openForm = (type, row) => { operationType.value = type; form.value = {}; formRegionPath.value = []; form.value.regionsId = ""; form.value.regionsld = ""; form.value.maintainer = userStore.nickName; formYYs.value.contactList = [ { @@ -994,6 +1062,23 @@ if (type === "edit") { getCustomer(row.id).then(res => { form.value = { ...res.data }; const regionIdForEdit = form.value.regionsId || form.value.regionsld; formRegionPath.value = findRegionPathById(regionTreeData.value, regionIdForEdit); if (!formRegionPath.value.length) { formRegionPath.value = findRegionPathByName( regionTreeData.value, form.value.regionsName || form.value.regions || "" ); } const selectedRegionId = formRegionPath.value.length > 0 ? formRegionPath.value[formRegionPath.value.length - 1] : ""; if (selectedRegionId && !form.value.regions) { form.value.regions = regionNodeMap.value.get(selectedRegionId)?.regionsName || ""; } form.value.regionsId = form.value.regionsId || selectedRegionId; form.value.regionsld = form.value.regionsld || form.value.regionsId || selectedRegionId; formYYs.value.contactList = res.data.contactPerson .split(",") .map((item, index) => { @@ -1029,6 +1114,10 @@ form.value.contactPhone = formYYs.value.contactList .map(item => item.contactPhone) .join(","); if (!form.value.regionsId && formRegionPath.value.length) { form.value.regionsId = formRegionPath.value[formRegionPath.value.length - 1]; } form.value.regionsld = form.value.regionsId || ""; addCustomer(form.value).then(res => { proxy.$modal.msgSuccess("æäº¤æå"); closeDia(); @@ -1043,6 +1132,10 @@ form.value.contactPhone = formYYs.value.contactList .map(item => item.contactPhone) .join(","); if (!form.value.regionsId && formRegionPath.value.length) { form.value.regionsId = formRegionPath.value[formRegionPath.value.length - 1]; } form.value.regionsld = form.value.regionsId || ""; updateCustomer(form.value).then(res => { proxy.$modal.msgSuccess("æäº¤æå"); closeDia(); @@ -1052,6 +1145,7 @@ // å ³éå¼¹æ¡ const closeDia = () => { proxy.resetForm("formRef"); formRegionPath.value = []; dialogFormVisible.value = false; }; // å¯¼åº @@ -1106,168 +1200,17 @@ }); }; // æå¼å访æéå¼¹çª const openReminderDialog = row => { currentCustomerId.value = row.id; reminderForm.customerName = row.customerName; reminderForm.reminderSwitch = false; reminderForm.reminderContent = ""; reminderForm.reminderTime = ""; // å°è¯è·åå·²æçå访æé getReturnVisit(row.id) .then(res => { if (res.code === 200 && res.data) { reminderForm.reminderSwitch = res.data.isEnabled === 1; reminderForm.reminderContent = res.data.content; reminderForm.reminderTime = res.data.reminderTime; reminderForm.id = res.data.id; } }) .catch(error => { console.error("è·åå访æé失败:", error); }); reminderDialogVisible.value = true; }; // å ³éå访æéå¼¹çª const closeReminderDialog = () => { proxy.resetForm("reminderFormRef"); reminderDialogVisible.value = false; }; const submitvalue = ref({}); // æäº¤å访æé const submitReminderForm = () => { console.log("æäº¤å访æéæ°æ®:", userStore.id, userStore); proxy.$refs.reminderFormRef.validate(valid => { if (valid) { if (reminderForm.id) { submitvalue.value = { id: reminderForm.id, customerId: currentCustomerId.value, isEnabled: reminderForm.reminderSwitch ? 1 : 0, content: reminderForm.reminderContent, reminderTime: reminderForm.reminderTime, remindUserId: userStore.id, }; } else { submitvalue.value = { customerId: currentCustomerId.value, isEnabled: reminderForm.reminderSwitch ? 1 : 0, content: reminderForm.reminderContent, reminderTime: reminderForm.reminderTime, remindUserId: userStore.id, }; } console.log("æäº¤å访æéæ°æ®:", submitvalue.value); // è°ç¨æ¥å£ addReturnVisit(submitvalue.value) .then(res => { if (res.code === 200) { proxy.$modal.msgSuccess("å访æé设置æå"); closeReminderDialog(); } else { proxy.$modal.msgError(res.msg || "设置失败"); } }) .catch(error => { console.error("设置å访æé失败:", error); proxy.$modal.msgError("设置失败"); }); } }); }; // æå¼æ´½è°è¿åº¦å¼¹çª const openNegotiationDialog = row => { negotiationForm.customerName = row.customerName; negotiationForm.customerId = row.id; negotiationForm.followUpMethod = ""; negotiationForm.followUpLevel = ""; negotiationForm.followUpTime = ""; negotiationForm.followerUserName = userStore.nickName; // é»è®¤å½åç»å½äºº negotiationForm.content = ""; // { // "customerId": 152, // "followUpMethod": "çµè¯æ²é", // "followUpLevel": "没ææå", // "followUpTime": "2026-03-04T15:30:00", // "followerUserName": "管çåè´¦å·", // "content": "111" // } negotiationDialogVisible.value = true; }; // å ³éæ´½è°è¿åº¦å¼¹çª const closeNegotiationDialog = () => { proxy.resetForm("negotiationFormRef"); // æ¸ é¤ç¼è¾ç¶æ delete negotiationForm.editIndex; delete negotiationForm.id; negotiationDialogVisible.value = false; }; // æäº¤æ´½è°è¿åº¦ const submitNegotiationForm = () => { proxy.$refs.negotiationFormRef.validate(valid => { if (valid) { // å¤ææ¯æ°å¢è¿æ¯ä¿®æ¹ const isEdit = negotiationForm.editIndex !== undefined; if (isEdit) { // ä¿®æ¹æä½ console.log("ä¿®æ¹æ´½è°è¿åº¦æ°æ®:", negotiationForm); // è¿éå¯ä»¥è°ç¨æ´æ°æ¥å£ // å®é 项ç®ä¸éè¦æ ¹æ®å端æ¥å£è¿è¡è°æ´ // 示ä¾ï¼updateCustomerFollow(negotiationForm).then(res => { // // æ´æ°æ¬å°æ°æ® // const index = negotiationForm.editIndex; // negotiationRecords.value[index] = { // followUpTime: negotiationForm.followUpTime, // followUpMethod: negotiationForm.followUpMethod, // followUpLevel: negotiationForm.followUpLevel, // followerUserName: negotiationForm.followerUserName, // content: negotiationForm.content, // id: negotiationForm.id, // }; // proxy.$modal.msgSuccess("ä¿®æ¹æå"); // closeNegotiationDialog(); // }); updateCustomerFollow(negotiationForm).then(res => { // æ´æ°æ¬å°æ°æ® getCustomer(negotiationForm.customerId).then(res => { // æ´æ°æ¬å°æ°æ® negotiationRecords.value = res.data.followUpList || []; }); }); proxy.$modal.msgSuccess("ä¿®æ¹æå"); closeNegotiationDialog(); } else { // æ°å¢æä½ console.log("æäº¤æ´½è°è¿åº¦æ°æ®:", negotiationForm); addCustomerFollow(negotiationForm).then(res => { // æ·»å æååæ´æ°è¯¦æ 页é¢çè¿åº¦è®°å½ const newRecord = { followUpTime: negotiationForm.followUpTime, followUpMethod: negotiationForm.followUpMethod, followUpLevel: negotiationForm.followUpLevel, followerUserName: negotiationForm.followerUserName, content: negotiationForm.content, }; negotiationRecords.value.unshift(newRecord); proxy.$modal.msgSuccess("æäº¤æå"); closeNegotiationDialog(); getList(); }); } } }); }; /* * å访æé / æ´½è°è¿åº¦åè½ï¼å ¥å£åå¼¹çªï¼å·²æéæ±æ³¨éï¼ä»¥ä¸æ¹æ³æä¸å¯ç¨ï¼ * - openReminderDialog / closeReminderDialog / submitReminderForm * - openNegotiationDialog / closeNegotiationDialog / submitNegotiationForm */ // const openReminderDialog = row => {}; // const closeReminderDialog = () => {}; // const submitReminderForm = () => {}; // const openNegotiationDialog = row => {}; // const closeNegotiationDialog = () => {}; // const submitNegotiationForm = () => {}; // æå¼è¯¦æ å¼¹çª const openDetailDialog = row => { @@ -1288,190 +1231,11 @@ detailDialogVisible.value = false; }; // ä¿®æ¹æ´½è°è®°å½ const editNegotiationRecord = (row, index) => { // å°å½åè®°å½æ°æ®å¡«å å°è¡¨å Object.assign(negotiationForm, { customerName: row.customerName, customerId: row.customerId, followUpMethod: row.followUpMethod, followUpLevel: row.followUpLevel, followUpTime: row.followUpTime, followerUserName: row.followerUserName, content: row.content, id: row.id, // è®°å½IDç¨äºæ´æ° editIndex: index, // è®°å½ç´¢å¼ç¨äºæ¬å°æ´æ° }); negotiationDialogVisible.value = true; }; // å 餿´½è°è®°å½ const deleteNegotiationRecord = (row, index) => { ElMessageBox.confirm("ç¡®å®è¦å é¤è¿æ¡æ´½è°è®°å½åï¼", "å é¤æç¤º", { confirmButtonText: "ç¡®å®", cancelButtonText: "åæ¶", type: "warning", }) .then(() => { // è¿éå¯ä»¥è°ç¨å 餿¥å£ // å®é 项ç®ä¸éè¦æ ¹æ®å端æ¥å£è¿è¡è°æ´ // 示ä¾ï¼deleteCustomerFollow(row.id).then(() => { // negotiationRecords.value.splice(index, 1); // proxy.$modal.msgSuccess("å 餿å"); // }); delCustomerFollow(row.id).then(() => { // å 餿ååæ´æ°æ¬å°æ°æ® getCustomer(row.customerId).then(res => { // æ´æ°æ¬å°æ°æ® negotiationRecords.value = res.data.followUpList || []; }); proxy.$modal.msgSuccess("å 餿å"); }); // æ¬å°å é¤ï¼æ¨¡æï¼ negotiationRecords.value.splice(index, 1); proxy.$modal.msgSuccess("å 餿å"); }) .catch(() => { proxy.$modal.msg("已忶å é¤"); }); }; // æå¼éä»¶å¼¹çª const openAttachmentDialog = row => { currentFollowRecord.value = row; // 转æ¢ä¸ºç¬¦åElement Plus fileListæ ¼å¼çæ°ç» currentAttachmentList.value = (row.fileList || []).map((file, index) => ({ name: file.fileName, url: file.fileUrl, size: file.fileSize, id: file.id, uid: file.id || index, status: "success", })); attachmentDialogVisible.value = true; }; // å ³ééä»¶å¼¹çª const closeAttachmentDialog = () => { attachmentDialogVisible.value = false; currentFollowRecord.value = {}; currentAttachmentList.value = []; }; // éä»¶ä¸ä¼ æå const handleAttachmentSuccess = (response, file, fileList) => { if (response.code === 200) { proxy.$modal.msgSuccess("ä¸ä¼ æå"); // æ´æ°å½åè®°å½çéä»¶å表 currentAttachmentList.value = fileList.map(item => ({ name: item.name, size: item.size, url: item.response?.data?.url || item.url, id: item.response?.data?.id, uid: item.uid, status: "success", })); // æ´æ°åè®°å½ä¸çfilesåæ®µ if (currentFollowRecord.value) { currentFollowRecord.value.files = [...currentAttachmentList.value]; } } else { proxy.$modal.msgError(response.msg || "ä¸ä¼ 失败"); } }; // éä»¶ä¸ä¼ 失败 const handleAttachmentError = (error, file, fileList) => { console.error("ä¸ä¼ 失败:", error); proxy.$modal.msgError("ä¸ä¼ 失败"); }; // éä»¶ç§»é¤ const handleAttachmentRemove = (file, fileList) => { currentAttachmentList.value = fileList; // æ´æ°åè®°å½ä¸çfilesåæ®µ if (currentFollowRecord.value) { currentFollowRecord.value.files = [...fileList]; } }; // éä»¶ä¸ä¼ åæ ¡éª const beforeAttachmentUpload = file => { const maxSize = 50 * 1024 * 1024; // 50MB if (file.size > maxSize) { proxy.$modal.msgError("æä»¶å¤§å°ä¸è½è¶ è¿50MB"); return false; } return true; }; // æ ¼å¼åæä»¶å¤§å° const formatFileSize = size => { if (size < 1024) { return size + " B"; } else if (size < 1024 * 1024) { return (size / 1024).toFixed(2) + " KB"; } else { return (size / (1024 * 1024)).toFixed(2) + " MB"; } }; // ä¸è½½éä»¶ const downloadAttachment = row => { if (row.url) { // proxy.download(row.url, {}, row.name); proxy.$download.name(row.url); } else { proxy.$modal.msgError("ä¸è½½é¾æ¥ä¸åå¨"); } }; // å é¤éä»¶ const deleteAttachment = (row, index) => { ElMessageBox.confirm("ç¡®å®è¦å é¤è¿ä¸ªéä»¶åï¼", "å é¤æç¤º", { confirmButtonText: "ç¡®å®", cancelButtonText: "åæ¶", type: "warning", }) .then(() => { // è°ç¨å端æ¥å£å é¤éä»¶ const deleteUrl = import.meta.env.VITE_APP_BASE_API + "/basic/customer-follow/file/" + row.id; fetch(deleteUrl, { method: "DELETE", headers: { Authorization: "Bearer " + getToken(), "Content-Type": "application/json", }, }) .then(response => response.json()) .then(res => { if (res.code === 200) { // å 餿ååæ´æ°æ¬å°æä»¶å表 currentAttachmentList.value.splice(index, 1); // æ´æ°åè®°å½ä¸çfilesåæ®µ if (currentFollowRecord.value) { currentFollowRecord.value.files = [ ...currentAttachmentList.value, ]; } proxy.$modal.msgSuccess("å 餿å"); } else { proxy.$modal.msgError(res.msg || "å é¤å¤±è´¥"); } }) .catch(error => { console.error("å é¤é件失败:", error); proxy.$modal.msgError("å é¤å¤±è´¥"); }); }) .catch(() => { proxy.$modal.msg("已忶å é¤"); }); }; /* * æ´½è°è¿åº¦è®°å½ & é件管çç¸å ³ï¼å·²éå ¥å£æ´ä½æ³¨éï¼ * - editNegotiationRecord / deleteNegotiationRecord * - openAttachmentDialog / closeAttachmentDialog / ...éä»¶ç¸å ³æ¹æ³ */ // è·åå½åæ¥æå¹¶æ ¼å¼å为 YYYY-MM-DD function getCurrentDate() { @@ -1483,11 +1247,186 @@ } onMounted(() => { fetchRegions(); getList(); }); watch(regionKeyword, value => { regionTreeRef.value?.filter((value || "").trim()); }); </script> <style scoped lang="scss"> .customer-split { display: flex; gap: 12px; } .left-panel { width: 260px; flex: 0 0 260px; background: #fff; border-radius: 6px; border: 1px solid #ebeef5; padding: 12px; height: calc(100vh - 140px); overflow: hidden; display: flex; flex-direction: column; } .right-panel { flex: 1; min-width: 0; } .toolbar-card { background: #ffffff; border: 1px solid #ebeef5; border-radius: 10px; padding: 14px 16px; margin-bottom: 12px; box-shadow: 0 2px 10px rgba(31, 35, 41, 0.04); } .right-search-form { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: nowrap; } .search-fields { display: flex; align-items: center; flex-wrap: nowrap; gap: 0; flex: 1; min-width: 0; } .toolbar-divider { width: 1px; align-self: stretch; background: linear-gradient(to bottom, transparent, #e4e7ed 15%, #e4e7ed 85%, transparent); } .action-buttons { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding-left: 2px; flex-shrink: 0; } .action-buttons :deep(.el-button) { margin-left: 0; min-width: 84px; } .table-card { background: #fff; border: 1px solid #ebeef5; border-radius: 10px; padding: 12px; box-shadow: 0 2px 10px rgba(31, 35, 41, 0.03); } @media (max-width: 1500px) { .right-search-form { flex-wrap: wrap; } .search-fields { flex-wrap: wrap; gap: 8px 0; } .toolbar-divider { display: none; } .action-buttons { width: 100%; border-top: 1px dashed #e4e7ed; padding-top: 10px; } } .left-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } .left-title { font-weight: 600; color: #303133; } .left-actions { display: flex; gap: 6px; } .left-actions :deep(.el-button) { margin-left: 0; padding: 5px 10px; } .left-search { margin-bottom: 10px; } .left-list { flex: 1; overflow: auto; padding-right: 4px; } .region-item { padding: 8px 10px; border-radius: 6px; cursor: pointer; color: #303133; user-select: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .region-item:hover { background: #f5f7fa; } .region-item.active { background: #ecf5ff; color: #409eff; font-weight: 600; } .left-list :deep(.el-tree) { background: transparent; } .left-list :deep(.el-tree-node__content) { height: 30px; border-radius: 6px; } .left-list :deep(.el-tree-node__content:hover) { background: #f5f7fa; } .empty-tip { padding: 16px 10px; color: #909399; font-size: 13px; } .detail-section { margin-bottom: 20px; padding: 15px; src/views/salesManagement/salesLedger/components/OtherAmountMaintenanceButton.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,278 @@ <template> <div style="display: inline-block;"> <el-button type="primary" plain @click="openDialog"> å ¶ä»éé¢ç»´æ¤ </el-button> <el-dialog v-model="visible" title="å ¶ä»éé¢ç»´æ¤" width="80%" :close-on-click-modal="false" @close="closeDialog" > <el-row :gutter="20"> <el-col :span="14"> <el-table :data="records" border v-loading="loading" height="55vh" > <el-table-column label="ç¼ç " prop="code" min-width="120" show-overflow-tooltip /> <el-table-column label="项ç®" prop="processName" min-width="180" show-overflow-tooltip /> <el-table-column label="æ°é" prop="quantity" min-width="110" :formatter="formattedNumber" /> <el-table-column label="åä»·(å )" prop="unitPrice" min-width="130" :formatter="formattedNumber" /> <el-table-column label="éé¢(å )" prop="amount" min-width="160" :formatter="formattedNumber" /> <el-table-column fixed="right" label="æä½" width="160" align="center"> <template #default="scope"> <el-button link type="primary" size="small" @click="handleEdit(scope.row)">ç¼è¾</el-button> <el-button link type="danger" size="small" @click="handleDelete(scope.row)">å é¤</el-button> </template> </el-table-column> </el-table> <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper" :page="page.current" :limit="page.size" @pagination="paginationChange" /> </el-col> <el-col :span="10"> <div style="padding: 8px 0;"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 10px;"> <div style="font-weight:600;"> {{ operationType === "add" ? "æ°å¢å ¶ä»éé¢" : "ç¼è¾å ¶ä»éé¢" }} </div> <el-button type="primary" plain size="small" @click="handleAdd" :disabled="operationType === 'add'" > æ°å¢ </el-button> </div> <el-form :model="form" label-width="120px" label-position="top" :rules="rules" ref="formRef" > <el-form-item label="ç¼ç "> <el-input v-model="form.code" placeholder="请è¾å ¥ç¼ç ï¼å¯éï¼" clearable /> </el-form-item> <el-form-item label="项ç®" prop="processName"> <el-input v-model="form.processName" placeholder="请è¾å ¥é¡¹ç®åç§°" clearable /> </el-form-item> <el-form-item label="æ°é" prop="quantity"> <el-input-number v-model="form.quantity" :min="0" :precision="2" style="width:100%" placeholder="请è¾å ¥æ°é" clearable @change="recalcAmount" /> </el-form-item> <el-form-item label="åä»·(å )" prop="unitPrice"> <el-input-number v-model="form.unitPrice" :min="0" :precision="2" style="width:100%" placeholder="请è¾å ¥åä»·" clearable @change="recalcAmount" /> </el-form-item> <el-form-item label="éé¢(å )"> <el-input v-model="form.amount" disabled /> </el-form-item> <div style="display:flex; justify-content:flex-end; gap: 10px; margin-top: 8px;"> <el-button @click="closeDialog">åæ¶</el-button> <el-button type="primary" @click="submitForm">ä¿å</el-button> </div> </el-form> </div> </el-col> </el-row> </el-dialog> </div> </template> <script setup> import { getCurrentInstance, reactive, ref } from "vue"; import { ElMessageBox } from "element-plus"; import pagination from "@/components/PIMTable/Pagination.vue"; import { salesLedgerProductProcessList, salesLedgerProductProcessAdd, salesLedgerProductProcessUpdate, salesLedgerProductProcessDelete, } from "@/api/salesManagement/salesLedger.js"; const { proxy } = getCurrentInstance(); const visible = ref(false); const loading = ref(false); const records = ref([]); const total = ref(0); const page = reactive({ current: 1, size: 10, }); const operationType = ref("add"); const formRef = ref(null); const form = reactive({ id: null, code: "", processName: "", quantity: 0, unitPrice: 0, amount: "0.00", }); const rules = reactive({ processName: [{ required: true, message: "请è¾å ¥é¡¹ç®åç§°", trigger: "change" }], quantity: [{ required: true, message: "请è¾å ¥æ°é", trigger: "blur" }], unitPrice: [{ required: true, message: "请è¾å ¥åä»·", trigger: "blur" }], }); const formattedNumber = (row, column, cellValue) => { return Number(cellValue || 0).toFixed(2); }; const recalcAmount = () => { const quantity = Number(form.quantity ?? 0) || 0; const unitPrice = Number(form.unitPrice ?? 0) || 0; form.amount = (quantity * unitPrice).toFixed(2); }; const resetForm = (type = "add") => { operationType.value = type; form.id = null; form.code = ""; form.processName = ""; form.quantity = 0; form.unitPrice = 0; form.amount = "0.00"; }; const fetchList = async () => { loading.value = true; try { const res = await salesLedgerProductProcessList({ current: page.current, size: page.size, }); const list = res?.records ?? res?.data?.records ?? []; const sum = res?.total ?? res?.data?.total ?? 0; records.value = list.map((item) => { const quantity = Number(item.quantity ?? 0) || 0; const unitPrice = Number(item.unitPrice ?? 0) || 0; const amount = Number(item.amount ?? quantity * unitPrice) || 0; return { id: item.id, code: item.code ?? item.remark ?? "", processName: item.processName ?? "", quantity, unitPrice, amount: amount.toFixed(2), }; }); total.value = sum; } finally { loading.value = false; } }; const openDialog = () => { visible.value = true; resetForm("add"); fetchList(); }; const closeDialog = () => { visible.value = false; resetForm("add"); }; const paginationChange = (obj) => { page.current = obj.page; page.size = obj.limit; fetchList(); }; const handleAdd = () => { resetForm("add"); }; const handleEdit = (row) => { if (!row) return; operationType.value = "edit"; form.id = row.id ?? null; form.code = row.code ?? ""; form.processName = row.processName ?? ""; form.quantity = Number(row.quantity ?? 0) || 0; form.unitPrice = Number(row.unitPrice ?? 0) || 0; recalcAmount(); }; const submitForm = () => { formRef.value?.validate((valid) => { if (!valid) return; const payload = { processName: form.processName, quantity: Number(form.quantity) || 0, unitPrice: Number(form.unitPrice) || 0, amount: Number(form.amount) || 0, remark: form.code, code: form.code, }; const req = operationType.value === "edit" ? salesLedgerProductProcessUpdate({ ...payload, id: form.id }) : salesLedgerProductProcessAdd(payload); req.then(() => { proxy.$modal.msgSuccess("ä¿åæå"); fetchList(); resetForm("add"); }); }); }; const handleDelete = (row) => { if (!row?.id) return; ElMessageBox.confirm("确认å é¤è¯¥è®°å½ï¼", "å é¤", { confirmButtonText: "确认", cancelButtonText: "åæ¶", type: "warning", }) .then(() => { return salesLedgerProductProcessDelete(row.id).then(() => { proxy.$modal.msgSuccess("å 餿å"); fetchList(); if (operationType.value === "edit" && form.id === row.id) { resetForm("add"); } }); }) .catch(() => { proxy.$modal.msg("已忶"); }); }; </script> src/views/salesManagement/salesLedger/components/ProcessFlowConfigSelectDialog.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,255 @@ <template> <el-dialog v-model="visible" title="鿩工èºè·¯çº¿é ç½®" width="1000px" :close-on-click-modal="false" @close="handleClose" > <el-row :gutter="20"> <el-col :span="24"> <div style="font-weight: 600; margin-bottom: 8px;">é ç½®</div> <div style="font-size: 12px; margin-bottom: 8px;"> <span v-if="boundRouteName" style="color: #67c23a;">å·²ç»å®ï¼{{ boundRouteName }}</span> <span v-else style="color: #e6a23c;">æªç»å®</span> </div> <el-select v-model="selectedRouteId" filterable clearable placeholder="è¯·éæ©å·¥èºè·¯çº¿" style="width: 100%;" @change="handleRouteChange" > <el-option v-for="cfg in routeList" :key="cfg.routeId" :label="cfg.processRouteName" :value="cfg.routeId" /> </el-select> <el-divider style="margin: 16px 0;" /> <div style="font-weight: 600; margin-bottom: 8px;">æ¥éª¤é¢è§</div> <div style="font-size: 12px; color: #909399; margin-bottom: 10px;"> æ ¹æ®æéé ç½®å±ç¤ºæµç¨å¾ </div> </el-col> <el-col :span="24"> <div class="process-diagram"> <div v-if="steps.length === 0" class="process-diagram-empty">ææ æ¥éª¤</div> <div v-for="(step, idx) in steps" :key="String(step.processId) + '_' + idx" class="process-diagram-segment" > <div class="process-diagram-node"> <div class="process-diagram-index">{{ idx + 1 }}</div> <div class="process-diagram-name">{{ step.processName }}</div> </div> <div v-if="idx < steps.length - 1" class="process-diagram-arrow">â</div> </div> </div> <div v-if="selectedRouteId === null" style="margin-top: 10px; font-size: 12px; color: #909399;"> 请å 鿩䏿¡å·²ç»´æ¤å¥½çå·¥èºè·¯çº¿ </div> </el-col> </el-row> <template #footer> <div class="dialog-footer"> <el-button @click="handleClose">åæ¶</el-button> <el-button type="primary" :loading="saving" @click="confirmSelect"> ç¡®å® </el-button> </div> </template> </el-dialog> </template> <script setup> import { computed, getCurrentInstance, ref, watch } from "vue"; import { salesProcessFlowConfigList, salesProcessFlowConfigItemList } from "@/api/salesManagement/salesProcessFlowConfig.js"; const emit = defineEmits(["update:visible", "confirm"]); const props = defineProps({ visible: { type: Boolean, default: false }, // æå¼å¼¹çªæ¶çåæ¾ï¼è¥ä¸å¡å·²ç»å®å·¥èºè·¯çº¿åä¼ å ¥è¯¥ routeIdï¼å¦åé»è®¤å±ç¤ºåè¡¨ç¬¬ä¸æ¡ defaultRouteId: { type: [Number, String, null], default: null }, // é¡µé¢æç¤ºï¼è®¢åå·²ç»å®çå·¥èºè·¯çº¿åç§° boundRouteName: { type: String, default: "" }, }); const { proxy } = getCurrentInstance(); const visible = computed({ get() { return props.visible; }, set(v) { emit("update:visible", v); }, }); const routeList = ref([]); const selectedRouteId = ref(null); const steps = ref([]); const saving = ref(false); const normalizeStepsFromApi = (list) => { if (!Array.isArray(list)) return []; return list.map((s, idx) => ({ stepId: s.stepId ?? s.id ?? null, processId: s.processId ?? s.process_id ?? s.id ?? null, processName: s.processName ?? s.process_name ?? s.name ?? "", sortNo: s.sortNo ?? idx + 1, })); }; const normalizeRouteList = (list) => { if (!Array.isArray(list)) return []; return list.map((r) => ({ routeId: r.routeId ?? r.id ?? null, processRouteName: r.processRouteName ?? r.routeName ?? r.name ?? "", isDefault: Boolean(r.isDefault), })); }; const fetchRouteList = async () => { // 鿩弹çªï¼å°½é䏿¬¡æ§æå ¨ï¼é¿å å页影åéæ©ä½éª const res = await salesProcessFlowConfigList({ current: 1, size: 1000 }); const records = res?.records ?? res?.data?.records ?? res?.data ?? res ?? []; routeList.value = normalizeRouteList(records).filter((r) => r.routeId !== null && r.routeId !== undefined && r.routeId !== ""); }; const fetchRouteSteps = async (routeId) => { if (!routeId) { steps.value = []; return; } const res = await salesProcessFlowConfigItemList(routeId); const raw = res?.data ?? res ?? []; steps.value = normalizeStepsFromApi(raw); }; watch( () => props.visible, async (v) => { if (v) { try { await fetchRouteList(); // åæ¾ç»å®ï¼ // 1. è¥ä¼ å ¥ defaultRouteIdï¼åä¼å 使ç¨å® // 2. å¦åä¼å é䏿 记为é»è®¤(isDefault=true)çå·¥èºè·¯çº¿ // 3. è¥é½æ²¡æï¼ååéä¸ºç¬¬ä¸æ¡ const first = routeList.value?.[0] ?? null; const defaultRoute = routeList.value.find((r) => r.isDefault) ?? first; const desired = props.defaultRouteId ?? (defaultRoute ? defaultRoute.routeId : null); selectedRouteId.value = desired ?? null; await fetchRouteSteps(selectedRouteId.value); } catch { proxy?.$modal?.msgError?.("è·åå·¥èºè·¯çº¿é 置失败"); } } } ); const handleRouteChange = async () => { await fetchRouteSteps(selectedRouteId.value); }; const handleClose = () => { emit("update:visible", false); saving.value = false; }; const confirmSelect = async () => { if (saving.value) return; if (selectedRouteId.value === null || selectedRouteId.value === undefined || selectedRouteId.value === "") { proxy?.$modal?.msgWarning?.("è¯·éæ©å·¥èºè·¯çº¿"); return; } saving.value = true; try { emit("confirm", selectedRouteId.value); } catch (e) { proxy?.$modal?.msgError?.("确认失败ï¼è¯·ç¨åéè¯"); } finally { saving.value = false; } }; </script> <style scoped> .process-diagram { display: flex; align-items: center; gap: 0; flex-wrap: nowrap; overflow-x: auto; padding: 10px 0; } .process-diagram-segment { display: flex; align-items: center; } .process-diagram-node { width: 160px; min-width: 160px; height: 78px; border: 1px solid #ebeef5; border-radius: 10px; background: #fff; display: flex; flex-direction: column; justify-content: center; padding: 10px 12px; margin-right: 10px; box-sizing: border-box; } .process-diagram-index { font-size: 12px; color: #909399; margin-bottom: 4px; } .process-diagram-name { font-size: 14px; font-weight: 600; color: #303133; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .process-diagram-arrow { font-size: 18px; color: #909399; margin-right: 14px; margin-left: -6px; } .process-diagram-empty { width: 100%; text-align: center; padding: 40px 0; color: #909399; border: 1px dashed #ebeef5; border-radius: 8px; } .dialog-footer { display: flex; justify-content: flex-end; gap: 10px; } </style> src/views/salesManagement/salesLedger/components/ProcessFlowMaintenanceButton.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,525 @@ <template> <div style="display: inline-block; margin-left: 8px;"> <el-button type="primary" plain @click="openDialog">å·¥èºæµç¨</el-button> <el-dialog v-model="visible" title="å·¥èºè·¯çº¿ä¸å·¥åºç»´æ¤" width="1280px" class="process-route-dialog" :close-on-click-modal="false" @close="closeDialog" > <el-row :gutter="16" class="dialog-main"> <el-col :span="10"> <div class="left-panel"> <div style="font-weight: 600; margin-bottom: 10px;">å·¥èºè·¯çº¿</div> <div class="route-toolbar route-toolbar-left"> <el-input v-model="routeKeyword" placeholder="æå·¥èºè·¯çº¿åç§°æ¥è¯¢" clearable @keyup.enter="handleRouteQuery" /> <el-button type="primary" @click="handleRouteQuery">æ¥è¯¢</el-button> <el-input v-model="routeNameDraft" placeholder="è¾å ¥åç§°åæ°å¢" clearable /> <el-button type="primary" plain @click="createRoute">æ°å¢</el-button> </div> <div class="left-table-wrap"> <el-table :data="routeList" border row-key="routeId" highlight-current-row height="100%" table-layout="fixed" @current-change="handleRouteSelect" > <el-table-column label="åºå·" width="56" align="center"> <template #default="scope">{{ scope.$index + 1 }}</template> </el-table-column> <el-table-column label="å·¥èºè·¯çº¿åç§°" min-width="150" prop="processRouteName" show-overflow-tooltip /> <el-table-column label="é»è®¤" width="56" align="center"> <template #default="scope"> <el-tag v-if="scope.row.isDefault" type="success" size="small">æ¯</el-tag> <span v-else>-</span> </template> </el-table-column> <el-table-column label="æä½" width="160" align="center"> <template #default="scope"> <el-button link type="primary" size="small" @click="editRoute(scope.row)">æ¹å</el-button> <el-button link type="primary" size="small" @click="setDefaultRoute(scope.row)">é»è®¤</el-button> <el-button link type="danger" size="small" @click="deleteRoute(scope.row)">å é¤</el-button> </template> </el-table-column> </el-table> </div> <pagination v-show="routeTotal > 0" :total="routeTotal" layout="total, sizes, prev, pager, next, jumper" :page="routePage.current" :limit="routePage.size" @pagination="handleRoutePaginationChange" /> </div> </el-col> <el-col :span="14"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;"> <div style="font-weight: 600;">å·¥åºç»´æ¤</div> <el-tag v-if="selectedRouteId" type="success" size="small">å½å路线ï¼{{ selectedRouteName }}</el-tag> <el-tag v-else type="info" size="small">请å 鿩工èºè·¯çº¿</el-tag> </div> <div class="route-toolbar"> <el-input v-model="processNameDraft" placeholder="è¾å ¥å·¥åºåç§°åæ°å¢" style="max-width: 260px;" clearable :disabled="!selectedRouteId" /> <el-button type="primary" plain :disabled="!selectedRouteId" @click="createProcessItem">æ°å¢å·¥åº</el-button> </div> <div class="process-diagram"> <div v-if="processItems.length === 0" class="process-diagram-empty">ææ å·¥åº</div> <div v-for="(step, idx) in processItems" :key="String(step.itemId) + '_' + idx" class="process-diagram-segment" > <div class="process-diagram-node"> <div class="process-diagram-index">{{ idx + 1 }}</div> <div class="process-diagram-name">{{ step.processName }}</div> </div> <div v-if="idx < processItems.length - 1" class="process-diagram-arrow">â</div> </div> </div> <el-table ref="processTableRef" :data="processItems" border row-key="itemId" height="420px" size="small"> <el-table-column label="åºå·" width="80" align="center"> <template #default="scope">{{ scope.$index + 1 }}</template> </el-table-column> <el-table-column label="å·¥åºåç§°" min-width="220" prop="processName" show-overflow-tooltip /> <el-table-column label="æä½" width="300" align="center"> <template #default="scope"> <el-button link type="primary" size="small" @click="editProcessItem(scope.row)">ç¼è¾</el-button> <el-button link type="danger" size="small" @click="deleteProcessItem(scope.row)">å é¤</el-button> </template> </el-table-column> </el-table> </el-col> </el-row> </el-dialog> </div> </template> <script setup> import { getCurrentInstance, ref, watch, onBeforeUnmount, nextTick } from "vue"; import { ElMessageBox } from "element-plus"; import Sortable from "sortablejs"; import { salesProcessFlowConfigList, salesProcessFlowConfigUpsert, salesProcessFlowConfigDelete, salesProcessFlowConfigSetDefault, salesProcessFlowConfigItemList, salesProcessFlowConfigItemUpsert, salesProcessFlowConfigItemSort, salesProcessFlowConfigItemDelete, } from "@/api/salesManagement/salesProcessFlowConfig.js"; const { proxy } = getCurrentInstance(); const visible = ref(false); let prevBodyOverflow = ""; let prevBodyOverflowY = ""; const lockBodyScroll = () => { // å åºå¤çï¼æäºåºæ¯ä¸ Element Plus ä¸ä¼å®å ¨ç¦æ¢èæ¯é¡µé¢æ»å¨ prevBodyOverflow = document.body.style.overflow || ""; prevBodyOverflowY = document.body.style.overflowY || ""; document.body.style.overflow = "hidden"; document.body.style.overflowY = "hidden"; }; const unlockBodyScroll = () => { document.body.style.overflow = prevBodyOverflow; document.body.style.overflowY = prevBodyOverflowY; }; watch(visible, (v) => { if (v) lockBodyScroll(); else unlockBodyScroll(); }); onBeforeUnmount(() => { unlockBodyScroll(); }); const routeKeyword = ref(""); const routeNameDraft = ref(""); const processNameDraft = ref(""); const routeList = ref([]); const routeTotal = ref(0); const routePage = ref({ current: 1, size: 10, }); const selectedRouteId = ref(null); const selectedRouteName = ref(""); const processItems = ref([]); const processTableRef = ref(null); let processStepsSortable = null; let isProcessingDrag = false; const destroyProcessSortable = () => { if (processStepsSortable) { processStepsSortable.destroy(); processStepsSortable = null; } }; const initProcessSortable = () => { destroyProcessSortable(); if (!processTableRef.value) return; const tbody = processTableRef.value?.$el?.querySelector(".el-table__body tbody") || processTableRef.value?.$el?.querySelector(".el-table__body-wrapper > table > tbody"); if (!tbody) return; processStepsSortable = new Sortable(tbody, { animation: 150, ghostClass: "sortable-ghost", draggable: ".el-table__row", handle: ".el-table__row", filter: ".el-button, .el-input, .el-select", preventOnFilter: true, onEnd: async (evt) => { if (isProcessingDrag) return; const { oldIndex, newIndex } = evt; if (oldIndex === newIndex) return; if (!selectedRouteId.value) return; if (!processItems.value[oldIndex]) return; isProcessingDrag = true; try { const arr = [...processItems.value]; const moving = arr.splice(oldIndex, 1)[0]; arr.splice(newIndex, 0, moving); processItems.value = arr; ensureSortNo(); if (!moving?.itemId) { proxy?.$modal?.msgError?.("å½åå·¥åºç¼ºå°IDï¼æ æ³æåº"); await fetchProcessItems(selectedRouteId.value); return; } // 使ç¨ä¸ç¨æåºæ¥å£ï¼é¿å upsert é æé夿°å¢ await salesProcessFlowConfigItemSort({ id: moving.itemId, dragSort: newIndex + 1, }); proxy?.$modal?.msgSuccess?.("顺åºè°æ´æå"); await fetchProcessItems(selectedRouteId.value); } finally { isProcessingDrag = false; } }, }); }; const normalizeRouteList = (list) => { if (!Array.isArray(list)) return []; return list.map((r) => ({ routeId: r.routeId ?? r.id ?? null, processRouteName: r.processRouteName ?? r.routeName ?? r.name ?? "", isDefault: Boolean(r.isDefault), })); }; const normalizeItemList = (list) => { if (!Array.isArray(list)) return []; return list.map((i, idx) => ({ itemId: i.itemId ?? i.id ?? null, routeId: i.routeId ?? i.processRouteId ?? selectedRouteId.value, processName: i.processName ?? i.name ?? "", sortNo: i.sortNo ?? idx + 1, })); }; const fetchRouteList = async () => { const res = await salesProcessFlowConfigList({ current: routePage.value.current, size: routePage.value.size, processRouteName: routeKeyword.value || undefined, }); const records = res?.records ?? res?.data?.records ?? []; const total = res?.total ?? res?.data?.total ?? 0; routeTotal.value = Number(total) || 0; routeList.value = normalizeRouteList(records); }; const handleRouteQuery = async () => { routePage.value.current = 1; await fetchRouteList(); }; const handleRoutePaginationChange = async (obj) => { routePage.value.current = obj.page; routePage.value.size = obj.limit; await fetchRouteList(); }; const fetchProcessItems = async (routeId) => { if (!routeId) { processItems.value = []; destroyProcessSortable(); return; } const res = await salesProcessFlowConfigItemList(routeId); const raw = res?.data ?? res ?? []; processItems.value = normalizeItemList(raw); ensureSortNo(); await nextTick(); initProcessSortable(); }; const openDialog = async () => { visible.value = true; selectedRouteId.value = null; selectedRouteName.value = ""; processItems.value = []; routePage.value.current = 1; await fetchRouteList(); }; const closeDialog = () => { visible.value = false; selectedRouteId.value = null; selectedRouteName.value = ""; processItems.value = []; routeNameDraft.value = ""; processNameDraft.value = ""; routeTotal.value = 0; destroyProcessSortable(); }; const handleRouteSelect = async (row) => { if (!row?.routeId) return; selectedRouteId.value = row.routeId; selectedRouteName.value = row.processRouteName; await fetchProcessItems(selectedRouteId.value); }; const createRoute = async () => { if (!routeNameDraft.value) { proxy?.$modal?.msgWarning("请å è¾å ¥å·¥èºè·¯çº¿åç§°"); return; } const payload = { processRouteName: routeNameDraft.value }; await salesProcessFlowConfigUpsert(payload); proxy?.$modal?.msgSuccess("å·¥èºè·¯çº¿æ°å¢æå"); routeNameDraft.value = ""; await handleRouteQuery(); }; const editRoute = async (row) => { const oldName = row?.processRouteName ?? ""; const { value } = await ElMessageBox.prompt("请è¾å ¥æ°çå·¥èºè·¯çº¿åç§°", "ä¿®æ¹å·¥èºè·¯çº¿", { inputValue: oldName, confirmButtonText: "确认", cancelButtonText: "åæ¶", }); await salesProcessFlowConfigUpsert({ routeId: row.routeId, processRouteName: value, }); proxy?.$modal?.msgSuccess("å·¥èºè·¯çº¿ä¿®æ¹æå"); await fetchRouteList(); if (selectedRouteId.value === row.routeId) selectedRouteName.value = value; }; const deleteRoute = async (row) => { await ElMessageBox.confirm("确认å é¤è¯¥å·¥èºè·¯çº¿ï¼", "å é¤", { confirmButtonText: "确认", cancelButtonText: "åæ¶", type: "warning", }); await salesProcessFlowConfigDelete(row.routeId); proxy?.$modal?.msgSuccess("å 餿å"); if (selectedRouteId.value === row.routeId) { selectedRouteId.value = null; selectedRouteName.value = ""; processItems.value = []; } // å é¤åè¥å½åé¡µè¢«æ¸ ç©ºï¼åéä¸é¡µ if (routeList.value.length <= 1 && routePage.value.current > 1) { routePage.value.current = routePage.value.current - 1; } await fetchRouteList(); }; const setDefaultRoute = async (row) => { await salesProcessFlowConfigSetDefault(row.routeId); proxy?.$modal?.msgSuccess("é»è®¤å·¥èºè·¯çº¿è®¾ç½®æå"); await fetchRouteList(); }; const ensureSortNo = () => { processItems.value = processItems.value.map((i, idx) => ({ ...i, sortNo: idx + 1 })); }; const createProcessItem = async () => { if (!selectedRouteId.value) { proxy?.$modal?.msgWarning("请å 鿩工èºè·¯çº¿"); return; } if (!processNameDraft.value) { proxy?.$modal?.msgWarning("请å è¾å ¥å·¥åºåç§°"); return; } await salesProcessFlowConfigItemUpsert({ routeId: selectedRouteId.value, processName: processNameDraft.value, sortNo: processItems.value.length + 1, }); proxy?.$modal?.msgSuccess("å·¥åºæ°å¢æå"); processNameDraft.value = ""; await fetchProcessItems(selectedRouteId.value); }; const editProcessItem = async (row) => { const { value } = await ElMessageBox.prompt("请è¾å ¥æ°çå·¥åºåç§°", "ä¿®æ¹å·¥åº", { inputValue: row.processName, confirmButtonText: "确认", cancelButtonText: "åæ¶", }); await salesProcessFlowConfigItemUpsert({ id: row.itemId, routeId: selectedRouteId.value, processName: value, sortNo: row.sortNo, }); proxy?.$modal?.msgSuccess("å·¥åºä¿®æ¹æå"); await fetchProcessItems(selectedRouteId.value); }; const deleteProcessItem = async (row) => { await ElMessageBox.confirm("确认å é¤è¯¥å·¥åºï¼", "å é¤", { confirmButtonText: "确认", cancelButtonText: "åæ¶", type: "warning", }); await salesProcessFlowConfigItemDelete(row.itemId); proxy?.$modal?.msgSuccess("å·¥åºå 餿å"); await fetchProcessItems(selectedRouteId.value); }; </script> <style scoped> .route-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; } .route-toolbar-left { flex-wrap: nowrap; } .route-toolbar-left :deep(.el-input) { width: 190px; } .process-route-dialog :deep(.el-dialog__body) { height: 760px; overflow: hidden; overflow-y: hidden; } .dialog-main { height: 100%; } .left-panel { height: 100%; display: flex; flex-direction: column; min-height: 0; } .left-table-wrap { flex: 1; min-height: 0; overflow: hidden; } .process-diagram { display: flex; align-items: center; gap: 0; flex-wrap: nowrap; overflow-x: auto; padding: 10px 0; } .process-diagram-segment { display: flex; align-items: center; } .process-diagram-node { width: 160px; min-width: 160px; height: 78px; border: 1px solid #ebeef5; border-radius: 10px; background: #fff; display: flex; flex-direction: column; justify-content: center; padding: 10px 12px; margin-right: 10px; box-sizing: border-box; } .process-diagram-index { font-size: 12px; color: #909399; margin-bottom: 4px; } .process-diagram-name { font-size: 14px; font-weight: 600; color: #303133; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .process-diagram-arrow { font-size: 18px; color: #909399; margin-right: 14px; margin-left: -6px; } .process-diagram-empty { width: 100%; text-align: center; padding: 24px 0; color: #909399; border: 1px dashed #ebeef5; border-radius: 8px; } </style> src/views/salesManagement/salesLedger/components/processCardPrint.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,261 @@ const PRINT_TITLE = "ç产æµç¨å¡(æå)"; const formatDisplayDate = (value) => { if (!value) return ""; const date = new Date(value); if (Number.isNaN(date.getTime())) return String(value); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}/${month}/${day}`; }; const getCurrentDate = () => { const date = new Date(); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}/${month}/${day}`; }; const escapeHtml = (value) => String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); const renderRouteHeader = (routeNodes) => { const columns = (Array.isArray(routeNodes) ? routeNodes : []) .sort((a, b) => (a?.dragSort ?? 0) - (b?.dragSort ?? 0)) .map((node) => `<th class="route-col">${escapeHtml(node?.processRouteItemName)}</th>`) .join(""); return columns || '<th class="route-col">å·¥åº</th>'; }; const renderRouteRow = (routeNodes) => { const columns = (Array.isArray(routeNodes) ? routeNodes : []) .sort((a, b) => (a?.dragSort ?? 0) - (b?.dragSort ?? 0)) .map(() => '<td class="route-col">次å</td>') .join(""); return columns || '<td class="route-col">次å</td>'; }; const renderRouteEmptyCells = (routeNodes) => { const columns = (Array.isArray(routeNodes) ? routeNodes : []) .sort((a, b) => (a?.dragSort ?? 0) - (b?.dragSort ?? 0)) .map(() => '<td class="route-col"></td>') .join(""); return columns || '<td class="route-col"></td>'; }; const renderItems = (items, startIndex, routeNodes, totalCols) => { const list = Array.isArray(items) ? items : []; if (list.length === 0) { return `<tr><td colspan="${totalCols}" style="text-align:center;">ææ æç»</td></tr>`; } const routeEmptyCells = renderRouteEmptyCells(routeNodes); return list .map( (item, index) => ` <tr> <td>${startIndex + index + 1}</td> <td class="no-wrap">${escapeHtml(item?.floorCode)}</td> <td class="no-wrap">${escapeHtml(item?.width)} * ${escapeHtml(item?.height)}</td> <td class="no-wrap">${escapeHtml(item?.quantity)}</td> <td class="no-wrap">${escapeHtml(item?.area)}</td> <td class="no-wrap">${escapeHtml(item?.processRequirement)}</td> ${routeEmptyCells} </tr> ` ) .join(""); }; const splitItemsByPage = (items, pageSize) => { const list = Array.isArray(items) ? items : []; if (list.length === 0) return [[]]; const pages = []; for (let i = 0; i < list.length; i += pageSize) { pages.push(list.slice(i, i + pageSize)); } return pages; }; export const printFinishedProcessCard = (cardData) => { const data = cardData ?? {}; const routeNodes = Array.isArray(data.routeNodes) ? data.routeNodes : []; const items = Array.isArray(data.items) ? data.items : []; const firstItem = items[0] ?? {}; const productName = firstItem.productDescription || ""; const totalCols = 6 + Math.max(routeNodes.length, 1); const signLabelCols = 2; const signBlankCols = Math.max(totalCols - 5 - signLabelCols, 1); const pageSize = 10; const itemPages = splitItemsByPage(items, pageSize); const totalPages = itemPages.length; const printWindow = window.open("", "_blank", "width=1200,height=900"); if (!printWindow) { throw new Error("æµè§å¨æ¦æªäºå¼¹çªï¼è¯·å 许弹çªåéè¯"); } const html = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>${PRINT_TITLE}</title> <style> body { margin: 0; padding: 0; font-family: "SimSun", serif; color: #222; } .page { width: 198mm; margin: 0 auto; padding: 6mm 3mm 5mm; box-sizing: border-box; page-break-after: always; } .page:last-child { page-break-after: auto; } .table-wrap { position: relative; } .page-mark { position: absolute; left: 0; top: -16px; z-index: 2; font-size: 12px; line-height: 1; background: #fff; padding: 0 2px; } .title { text-align: center; font-size: 18px; font-weight: 700; margin-bottom: 4px; line-height: 1.2; } .sub-title { font-size: 14px; } table { width: 100%; border-collapse: collapse; table-layout: auto; } td, th { border: 0.8px solid #6f7f95; padding: 2px 4px; font-size: 12px; text-align: center; vertical-align: middle; word-break: break-all; } .left { text-align: left; } .no-wrap { white-space: nowrap; word-break: keep-all; } .route-col { min-width: 48px; white-space: nowrap; word-break: keep-all; } .section-title { font-weight: 700; text-align: left; padding-left: 6px; } .sign-label { text-align: left; padding-left: 8px; vertical-align: top; line-height: 1.2; } .sign-blank { vertical-align: top; min-height: 18px; } .order-req-content { min-height: 82px; vertical-align: top; padding-top: 6px; } .sign-row td { height: 18px; } .order-require-title { width: 34px; padding: 0; } .order-require-title-text { display: flex; height: 100%; flex-direction: column; justify-content: center; align-items: center; line-height: 1.25; letter-spacing: 0; font-weight: 500; } .footer { margin-top: 10px; font-size: 12px; line-height: 1.7; padding: 0 2px; } .footer-row { display: flex; justify-content: space-between; } .footer-item { width: 33%; } .continued { text-align: right; font-size: 12px; padding-right: 8px; } @media print { @page { size: A4 portrait; margin: 8mm; } .page { width: 100%; margin: 0; padding: 0; } } </style> </head> <body> ${itemPages .map((pageItems, pageIndex) => { const isLastPage = pageIndex === totalPages - 1; const startIndex = pageIndex * pageSize; return ` <div class="page"> <div class="title">鹤å£å¤©æ²é¢åç»çå<br /><span class="sub-title">ç产æµç¨å¡(æå)</span></div> <div class="table-wrap"> <div class="page-mark">第${pageIndex + 1}页ï¼å ±${totalPages}页</div> <table> <thead> <tr> <td colspan="5" class="left">订åç¼å·:${escapeHtml(data.salesContractNo)}</td> <td colspan="${totalCols - 5}" class="left">äº¤è´§æ¥æ:${escapeHtml(formatDisplayDate(data.deliveryDate))}</td> </tr> <tr> <td colspan="5" class="left">客æ·åç§°:${escapeHtml(data.customerName)}</td> <td colspan="${totalCols - 5}" class="left">å·¥èºæµç¨:${escapeHtml(data.processPathDisplay)}</td> </tr> <tr> <th rowspan="2" style="width:6%;">订åº</th> <th rowspan="2" style="width:22%;" class="no-wrap">楼å±ç¼å·</th> <th rowspan="2" style="width:20%;" class="no-wrap">宽(å¼§é¿)*é«</th> <th rowspan="2" style="width:8%;" class="no-wrap">æ°é</th> <th rowspan="2" style="width:8%;" class="no-wrap">é¢ç§¯</th> <th rowspan="2" style="width:20%;" class="no-wrap">æç»å å·¥è¦æ±</th> ${renderRouteHeader(routeNodes)} </tr> <tr>${renderRouteRow(routeNodes)}</tr> </thead> <tbody> <tr> <td colspan="${totalCols}" class="section-title">产ååç§°:${escapeHtml(productName)}</td> </tr> ${renderItems(pageItems, startIndex, routeNodes, totalCols)} ${ isLastPage ? `<tr> <td colspan="3" class="left"><strong>å计:</strong></td> <td>${escapeHtml(data.totalQuantity)}</td> <td>${escapeHtml(data.totalArea)}</td> <td colspan="${signLabelCols}" class="sign-label">å®å·¥ç¾å</td> <td colspan="${signBlankCols}" class="sign-blank"></td> </tr> <tr class="sign-row"> <td rowspan="3" class="order-require-title"> <div class="order-require-title-text"> <span>订å</span> <span>å å·¥</span> <span>è¦æ±</span> </div> </td> <td colspan="4" rowspan="3" class="left order-req-content">${escapeHtml(data.orderProcessRequirement)}</td> <td colspan="${signLabelCols}" class="sign-label">è´¨æ£ç¾å</td> <td colspan="${signBlankCols}" class="sign-blank"></td> </tr> <tr class="sign-row"> <td colspan="${signLabelCols}" class="sign-label">æ¥æ¶ç¾å</td> <td colspan="${signBlankCols}" class="sign-blank"></td> </tr> <tr class="sign-row"> <td colspan="${signLabelCols}" class="sign-label">çäº§æ¥æ</td> <td colspan="${signBlankCols}" class="sign-blank"></td> </tr>` : `<tr><td colspan="${totalCols}" class="continued">ä¸é¡µç»...</td></tr>` } </tbody> </table> </div> ${ isLastPage ? `<div class="footer"> <div class="footer-row"> <div class="footer-item">å¶åå:${escapeHtml(data.register)}</div> <div class="footer-item">å®¡æ ¸å:${escapeHtml(data.register)}</div> <div class="footer-item">å·¥èºå:${escapeHtml(data.technician ?? "")}</div> </div> <div class="footer-row"> <div class="footer-item">å¶åæ¥æ:${escapeHtml(formatDisplayDate(data.registerDate))}</div> <div class="footer-item">å®¡æ ¸æ¥æ:${escapeHtml(formatDisplayDate(data.registerDate))}</div> <div class="footer-item">æå°æ¥æ:${getCurrentDate()}</div> </div> </div>` : "" } </div>`; }) .join("")} </body> </html> `; printWindow.document.write(html); printWindow.document.close(); printWindow.onload = () => { setTimeout(() => { printWindow.focus(); printWindow.print(); printWindow.close(); }, 300); }; }; src/views/salesManagement/salesLedger/components/salesDeliveryPrint.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,276 @@ const PRINT_TITLE = "éå®åè´§å"; const escapeHtml = (value) => String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); const toNumber = (value) => { const num = Number(value); return Number.isFinite(num) ? num : 0; }; const formatDisplayDate = (value) => { if (!value) return ""; const date = new Date(value); if (Number.isNaN(date.getTime())) return String(value); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}/${month}/${day}`; }; const getItemArea = (item) => toNumber(item?.area || item?.settleTotalArea || item?.actualTotalArea); const getOrderNo = (data, row, item) => item?.salesContractNo || item?.orderNo || data?.salesContractNo || row?.salesContractNo || ""; const splitItemsByPage = (items, pageSize) => { const list = Array.isArray(items) ? items : []; if (list.length === 0) return [[]]; const pages = []; for (let i = 0; i < list.length; i += pageSize) { pages.push(list.slice(i, i + pageSize)); } return pages; }; const normalizeInvoiceData = (raw, selectedRow) => { const data = raw ?? {}; const groups = Array.isArray(data.groups) ? data.groups : []; if (!groups.length) return data; const items = groups.flatMap((group) => (Array.isArray(group?.items) ? group.items : []).map((item) => ({ ...item, productDescription: group?.productName || item?.productDescription || "", salesContractNo: group?.salesContractNo || item?.salesContractNo || "", widthHeight: item?.widthHeight || "", })) ); return { ...data, items, customerName: data.customerName || selectedRow?.customerName || "", contactPerson: data.contactPerson || selectedRow?.contactPerson || "", contactPhone: data.contactPhone || selectedRow?.contactPhone || "", deliveryAddress: data.companyAddress || data.deliveryAddress || data.shippingAddress || selectedRow?.deliveryAddress || "", shipmentNo: data.externalOrderNo || data.shipmentNo || "", register: data.orderMaker || data.register || selectedRow?.entryPersonName || "", registerDate: data.executionDate || data.registerDate || data.entryDate || selectedRow?.entryDate || "", }; }; const groupByProduct = (items, data, row) => { const list = Array.isArray(items) ? items : []; const map = new Map(); list.forEach((item) => { const key = `${item?.productDescription || ""}__${getOrderNo(data, row, item)}`; if (!map.has(key)) { map.set(key, { productName: item?.productDescription || "", orderNo: getOrderNo(data, row, item), items: [], }); } map.get(key).items.push(item); }); return Array.from(map.values()); }; const renderItemRows = (items, startIndex) => items .map((item, idx) => { const sizeText = item?.widthHeight ? escapeHtml(item.widthHeight) : item?.width || item?.height ? `${escapeHtml(item?.width)} * ${escapeHtml(item?.height)}` : ""; return ` <tr> <td>${startIndex + idx + 1}</td> <td class="left">${escapeHtml(item?.floorCode)}</td> <td>${sizeText}</td> <td>${toNumber(item?.quantity) || ""}</td> <td>${getItemArea(item) ? getItemArea(item).toFixed(2) : ""}</td> <td class="left">${escapeHtml(item?.remark)}</td> <td class="left">${escapeHtml(item?.processRequirement)}</td> </tr> `; }) .join(""); export const printSalesDeliveryNote = (rawData, selectedRow = {}) => { const data = normalizeInvoiceData(rawData, selectedRow); const allItems = Array.isArray(data.items) ? data.items : []; const pageSize = 18; const itemPages = splitItemsByPage(allItems, pageSize); const totalPages = itemPages.length; const printWindow = window.open("", "_blank", "width=1200,height=900"); if (!printWindow) { throw new Error("æµè§å¨æ¦æªäºå¼¹çªï¼è¯·å 许弹çªåéè¯"); } const html = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>${PRINT_TITLE}</title> <style> body { margin: 0; padding: 0; font-family: "SimSun", serif; color: #222; } .page { width: 198mm; margin: 0 auto; padding: 4mm 4mm 6mm; box-sizing: border-box; page-break-after: always; } .page:last-child { page-break-after: auto; } .head-top { display: grid; grid-template-columns: 1fr auto 1fr; align-items: end; margin-bottom: 1px; } .factory { grid-column: 2; text-align: center; font-size: 20px; font-weight: 700; line-height: 1.2; } .page-mark { grid-column: 3; justify-self: end; font-size: 12px; margin-right: 8mm; margin-bottom: 1px; } .head-mid { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; margin-bottom: 2px; } .head-mid-left { font-size: 13px; text-align: left; } .head-mid-title { font-size: 20px; font-weight: 700; text-align: center; } .head-mid-right { font-size: 13px; text-align: right; padding-right: 8mm; } table { width: 100%; border-collapse: collapse; table-layout: fixed; border: 1px solid #222; } td, th { border: 1px solid #222; padding: 2px 4px; font-size: 13px; text-align: center; vertical-align: middle; } .left { text-align: left; } .group-title td { font-weight: 700; } .subtotal td, .total-row td { font-weight: 700; } .empty td { height: 120px; color: #666; } .footer { margin-top: 6px; display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 6px; font-size: 13px; } @media print { @page { size: A4 portrait; margin: 8mm; } .page { width: 100%; margin: 0; padding: 0; } } </style> </head> <body> ${itemPages .map((pageItems, pageIndex) => { const pageGroups = groupByProduct(pageItems, data, selectedRow); let serial = pageIndex * pageSize; const totalQty = toNumber(data.totalQuantity) || allItems.reduce((s, it) => s + toNumber(it?.quantity), 0); const totalArea = toNumber(data.totalArea) || allItems.reduce((s, it) => s + getItemArea(it), 0); return ` <div class="page"> <div class="head-top"> <div></div> <div class="factory">鹤å£å¤©æ²é¢åç»çå</div> <div class="page-mark">第${pageIndex + 1}页,å ±${totalPages}页</div> </div> <div class="head-mid"> <div class="head-mid-left">对æ¹åå·: ${escapeHtml(data.deliveryNo || data.shippingNo || selectedRow.expressNumber || "")}</div> <div class="head-mid-title">éå®åè´§å</div> <div class="head-mid-right">åè´§åå·: ${escapeHtml(data.shipmentNo || data.deliveryNo || "")}</div> </div> <table> <tr> <td class="left" colspan="4">客æ·åç§°: ${escapeHtml(data.customerName || selectedRow.customerName || "")}</td> <td class="left" colspan="3">è系人: ${escapeHtml(data.contactPerson || selectedRow.contactPerson || "")}</td> </tr> <tr> <td class="left" colspan="4">åè´§å°å: ${escapeHtml(data.deliveryAddress || data.shippingAddress || selectedRow.deliveryAddress || "")}</td> <td class="left" colspan="3">èç³»çµè¯: ${escapeHtml(data.contactPhone || selectedRow.contactPhone || "")}</td> </tr> <tr> <th style="width:8%;">åºå·</th> <th style="width:22%;">楼å±ç¼å·</th> <th style="width:20%;">宽(å¼§é¿)*é«</th> <th style="width:10%;">æ°é</th> <th style="width:12%;">é¢ç§¯</th> <th style="width:10%;">夿³¨</th> <th style="width:18%;">å å·¥è¦æ±</th> </tr> ${ pageGroups.length ? pageGroups .map((group) => { const subQty = group.items.reduce((s, it) => s + toNumber(it?.quantity), 0); const subArea = group.items.reduce((s, it) => s + getItemArea(it), 0); const rows = renderItemRows(group.items, serial); serial += group.items.length; return ` <tr class="group-title"> <td colspan="5" class="left">产ååç§°: ${escapeHtml(group.productName)}</td> <td colspan="2" class="left">订åç¼å·: ${escapeHtml(group.orderNo)}</td> </tr> ${rows} <tr class="subtotal"> <td colspan="3">å°è®¡:</td> <td>${subQty || ""}</td> <td>${subArea ? subArea.toFixed(2) : ""}</td> <td colspan="2"></td> </tr> `; }) .join("") : `<tr class="empty"><td colspan="7">ææ æç»</td></tr>` } ${ pageIndex === totalPages - 1 ? ` <tr class="total-row"> <td colspan="3">å计:</td> <td>${totalQty || ""}</td> <td>${totalArea ? totalArea.toFixed(2) : ""}</td> <td colspan="2"></td> </tr> ` : "" } </table> ${ pageIndex === totalPages - 1 ? ` <div class="footer"> <div>å¶ å å: ${escapeHtml(data.register || selectedRow.entryPersonName || "")}</div> <div>å¶åæ¥æ: ${escapeHtml(formatDisplayDate(data.registerDate || data.entryDate || selectedRow.entryDate))}</div> <div>客æ·ç¾å:</div> <div>ç¾æ¶æ¥æ:</div> </div> ` : "" } </div> `; }) .join("")} </body> </html> `; printWindow.document.write(html); printWindow.document.close(); printWindow.onload = () => { setTimeout(() => { printWindow.focus(); printWindow.print(); printWindow.close(); }, 300); }; }; src/views/salesManagement/salesLedger/components/salesLabelPrint.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,148 @@ const PRINT_TITLE = "é宿 ç¾"; const escapeHtml = (value) => String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); const splitByPage = (items, pageSize) => { const list = Array.isArray(items) ? items : []; if (list.length === 0) return [[]]; const pages = []; for (let i = 0; i < list.length; i += pageSize) { pages.push(list.slice(i, i + pageSize)); } return pages; }; const renderLabelCard = (item) => ` <div class="label-card"> <div class="line customer">${escapeHtml(item?.customerName)}</div> <div class="line order">${escapeHtml(item?.salesContractNo)}</div> <div class="line product">${escapeHtml(item?.productName)}</div> <div class="line spec">${escapeHtml(item?.specification)}</div> <div class="line address">${escapeHtml(item?.floorCode)}</div> </div> `; export const printSalesLabel = (rawList = []) => { const list = Array.isArray(rawList) ? rawList : []; if (!list.length) { throw new Error("æ ç¾æ°æ®ä¸ºç©ºï¼æ æ³æå°"); } const pageSize = 18; // 3 å * 6 è¡ï¼50x40mmï¼ const pages = splitByPage(list, pageSize); const printWindow = window.open("", "_blank", "width=1200,height=900"); if (!printWindow) { throw new Error("æµè§å¨æ¦æªäºå¼¹çªï¼è¯·å 许弹çªåéè¯"); } const html = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>${PRINT_TITLE}</title> <style> body { margin: 0; padding: 0; font-family: "SimSun", serif; color: #222; } .page { width: 210mm; min-height: 297mm; margin: 0 auto; padding: 8mm 7mm; box-sizing: border-box; page-break-after: always; } .page:last-child { page-break-after: auto; } .grid { display: grid; grid-template-columns: repeat(3, 50mm); grid-auto-rows: 40mm; gap: 2mm; justify-content: start; } .label-card { border: 1px solid #ddd; border-radius: 3mm; padding: 1.8mm 2.2mm; width: 50mm; height: 40mm; box-sizing: border-box; display: grid; grid-template-rows: auto auto auto auto 1fr; row-gap: 0.9mm; } .line { font-weight: 700; line-height: 1.15; word-break: break-all; margin: 0; } .customer { font-size: 4.0mm; } .order { font-size: 5.0mm; letter-spacing: 0; } .product { font-size: 4.5mm; } .spec { font-size: 5.0mm; letter-spacing: 0; } .address { font-size: 3.8mm; } @media print { @page { size: A4 portrait; margin: 0; } .page { width: 100%; min-height: 0; margin: 0; padding: 8mm 7mm; } } </style> </head> <body> ${pages .map( (pageList) => ` <div class="page"> <div class="grid"> ${pageList.map((item) => renderLabelCard(item)).join("")} </div> </div> ` ) .join("")} </body> </html>`; printWindow.document.write(html); printWindow.document.close(); printWindow.onload = () => { setTimeout(() => { printWindow.focus(); printWindow.print(); printWindow.close(); }, 300); }; }; src/views/salesManagement/salesLedger/components/salesOrderPrint.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,443 @@ const PRINT_TITLE = "éå®è®¢å"; const formatDisplayDate = (value) => { if (!value) return ""; const date = new Date(value); if (Number.isNaN(date.getTime())) return String(value); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}/${month}/${day}`; }; const getCurrentDateTime = () => { const date = new Date(); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}/${month}/${day}`; }; const escapeHtml = (value) => String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); const toNumber = (value) => { const num = Number(value); return Number.isFinite(num) ? num : 0; }; const formatMoney = (value) => { const num = toNumber(value); return num.toFixed(2); }; const formatArea = (value) => { const num = toNumber(value); return num ? num.toFixed(2) : ""; }; const formatQty = (value) => { const num = toNumber(value); return num ? String(num) : ""; }; const getItemAmount = (item) => { const fromPrice = toNumber(item?.taxInclusiveUnitPrice) * toNumber(item?.quantity); const amount = toNumber(item?.taxInclusiveTotalPrice || item?.amount || fromPrice); return amount; }; const getItemArea = (item) => toNumber(item?.area || item?.settleTotalArea || item?.actualTotalArea); const getDeliveryAddress = (data) => data?.companyAddress || data?.deliveryAddress || data?.shippingAddress || data?.address || data?.shipAddress || ""; const normalizeRequirementText = (value) => { const text = String(value ?? "").trim(); if (!text) return ""; return text .replace(/^å å·¥è¦æ±å夿³¨[:ï¼]\s*/g, "") .replace(/^å å·¥è¦æ±[:ï¼]\s*/g, "") .replace(/^夿³¨[:ï¼]\s*/g, ""); }; const extractOtherFees = (data, items) => { const source = []; if (Array.isArray(data?.otherFees)) source.push(...data.otherFees); if (Array.isArray(data?.otherAmounts)) source.push(...data.otherAmounts); if (Array.isArray(data?.otherAmountList)) source.push(...data.otherAmountList); if (Array.isArray(data?.otherAmountProjects)) source.push(...data.otherAmountProjects); if (Array.isArray(data?.salesProductProcessList)) source.push(...data.salesProductProcessList); (Array.isArray(items) ? items : []).forEach((item) => { if (Array.isArray(item?.salesProductProcessList)) source.push(...item.salesProductProcessList); if (Array.isArray(item?.otherAmounts)) source.push(...item.otherAmounts); }); const map = new Map(); source.forEach((fee) => { const name = String(fee?.feeName || fee?.processName || fee?.name || fee?.itemName || "").trim(); if (!name) return; const quantity = toNumber(fee?.quantity || fee?.num); const unitPrice = toNumber(fee?.unitPrice || fee?.price); const amount = toNumber(fee?.amount || fee?.totalPrice) || (quantity && unitPrice ? quantity * unitPrice : 0); const key = name; if (!map.has(key)) { map.set(key, { name, quantity: 0, unitPrice: 0, amount: 0 }); } const row = map.get(key); row.quantity += quantity; row.unitPrice = unitPrice || row.unitPrice; row.amount += amount; }); return Array.from(map.values()); }; const renderOtherFeeRows = (rows) => { const list = Array.isArray(rows) ? rows : []; if (list.length === 0) { return `<tr><td></td><td></td><td></td><td></td></tr>`; } return list .map((row) => { const name = escapeHtml(row.name); const unitPrice = row.unitPrice ? formatMoney(row.unitPrice) : ""; const quantity = row.quantity ? String(row.quantity) : ""; const amount = row.amount ? formatMoney(row.amount) : ""; return `<tr><td class="other-fee-shift-left">${name}</td><td class="other-fee-shift-left">${unitPrice}</td><td class="other-fee-shift-left">${quantity}</td><td>${amount}</td></tr>`; }) .join(""); }; const splitItemsByPage = (items, pageSize) => { const list = Array.isArray(items) ? items : []; if (list.length === 0) return [[]]; const pages = []; for (let i = 0; i < list.length; i += pageSize) { pages.push(list.slice(i, i + pageSize)); } return pages; }; const renderRows = (items, startIndex) => { const list = Array.isArray(items) ? items : []; if (list.length === 0) { return ` <tr> <td colspan="8" class="empty">ææ æç»</td> </tr> `; } return list .map((item, idx) => { const width = escapeHtml(item?.width); const height = escapeHtml(item?.height); const sizeText = width || height ? `${width}*${height}` : ""; const unitPrice = formatMoney(item?.taxInclusiveUnitPrice || item?.unitPrice); const amount = formatMoney(getItemAmount(item)); return ` <tr> <td>${startIndex + idx + 1}</td> <td>${escapeHtml(item?.floorCode)}</td> <td>${sizeText}</td> <td>${formatQty(item?.quantity)}</td> <td>${formatArea(item?.area || item?.settleTotalArea || item?.actualTotalArea)}</td> <td>${unitPrice}</td> <td>${amount}</td> <td>${escapeHtml(item?.processRequirement)}</td> </tr> `; }) .join(""); }; export const printSalesOrder = (orderData) => { const data = orderData ?? {}; const items = Array.isArray(data.items) ? data.items : []; const pageSize = 15; const pages = splitItemsByPage(items, pageSize); const totalPages = pages.length; const subtotalQuantity = toNumber(data.subtotalQuantity); const subtotalArea = toNumber(data.subtotalArea); const subtotalAmount = toNumber(data.subtotalAmount); const totalQuantity = toNumber(data.totalQuantity) || items.reduce((sum, item) => sum + toNumber(item?.quantity), 0); const totalArea = toNumber(data.totalArea) || items.reduce((sum, item) => sum + getItemArea(item), 0); const totalAmount = toNumber(data.totalAmount) || items.reduce((sum, item) => sum + getItemAmount(item), 0); const otherFees = extractOtherFees(data, items); const printWindow = window.open("", "_blank", "width=1200,height=900"); if (!printWindow) { throw new Error("æµè§å¨æ¦æªäºå¼¹çªï¼è¯·å 许弹çªåéè¯"); } const html = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>${PRINT_TITLE}</title> <style> body { margin: 0; padding: 0; font-family: "SimSun", serif; color: #111; } .page { width: 198mm; min-height: 186mm; margin: 0 auto; padding: 5mm 4mm 16mm; box-sizing: border-box; page-break-after: always; position: relative; } .page:last-child { page-break-after: auto; } .title-main { text-align: center; font-size: 22px; font-weight: 700; letter-spacing: 1px; line-height: 1.15; margin-top: 1mm; } .title-sub { text-align: center; font-size: 22px; font-weight: 700; letter-spacing: 6px; line-height: 1.15; margin: 1mm 0 4mm; } table { width: 100%; border-collapse: collapse; table-layout: fixed; border: 1px solid #222; } .detail-table { border-right: 1px solid #222 !important; } .detail-table-wrap { position: relative; margin-top: -1px; } .detail-table-wrap::after { content: none; } .sheet-wrap { position: relative; overflow: visible; } .sheet-wrap::after { content: none; } td, th { border: 1px solid #222; padding: 2px 4px; font-size: 13px; text-align: center; vertical-align: middle; } tr > td:first-child, tr > th:first-child { border-left: 1px solid #222 !important; } tr > td:last-child, tr > th:last-child { border-right: 1px solid #222 !important; } .left { text-align: left; } .bold { font-weight: 700; } .cell-title { font-weight: 700; white-space: nowrap; } .product-row td { font-size: 13px; font-weight: 700; } .empty { height: 140px; color: #777; } .large-row td { height: 46px; vertical-align: top; } .other-fee-content-row td { height: auto !important; } .total-row td { font-weight: 700; font-size: 13px; line-height: 1.2; } .footer { margin-top: 5px; display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; font-size: 13px; line-height: 1.7; } .footer b { display: inline-block; min-width: 74px; } .customer-sign-row { margin-top: 6px; display: grid; grid-template-columns: 1fr 1fr 1fr; } .customer-sign-cell { text-align: left; } .customer-sign { font-size: 13px; font-weight: 700; line-height: 1.2; } .right { text-align: right; } .footer-page-right { display: block; text-align: right; font-size: 12px; } .footer-pair { display: flex; align-items: center; justify-content: space-between; gap: 8px; } .other-fee-cell { white-space: normal; word-break: break-all; line-height: 1.35; vertical-align: top; } .other-fee-header { font-weight: 700; } .other-fee-header-grid { display: grid; grid-template-columns: repeat(4, 1fr); align-items: center; text-align: center; } .other-fee-header-row td { border-bottom: none !important; } .other-fee-content-row td { border-top: none !important; } .other-fee-inner { width: 100%; border-collapse: collapse; table-layout: fixed; border: none !important; } .other-fee-inner td { border: none !important; padding: 0 1px; text-align: center; vertical-align: middle; white-space: normal; word-break: break-all; line-height: 1.1; } .other-fee-inner tr > td:first-child, .other-fee-inner tr > td:last-child { border-left: none !important; border-right: none !important; } .other-fee-shift-left { position: relative; left: -30px; } @media print { @page { size: A4 landscape; margin: 6mm; } .page { width: 100%; margin: 0; padding: 0 0 14mm; min-height: 0; } } </style> </head> <body> ${pages .map((pageItems, pageIndex) => { const isLastPage = pageIndex === totalPages - 1; const startIndex = pageIndex * pageSize; return ` <div class="page"> <div class="title-main">${escapeHtml(data.companyName || "鹤å£å¤©æ²é¢åç»çå")}</div> <div class="title-sub">éå®è®¢å</div> <div class="sheet-wrap"> <table> <colgroup> <col style="width: 14%;" /> <col style="width: 20%;" /> <col style="width: 20%;" /> <col style="width: 14%;" /> <col style="width: 32%;" /> </colgroup> <tr> <td class="cell-title">客æ·åç§°:</td> <td class="left">${escapeHtml(data.customerName)}</td> <td class="cell-title">项ç®åç§°:</td> <td class="left">${escapeHtml(data.projectName)}</td> <td class="left"><span class="cell-title">ä¸ å¡ å:</span> ${escapeHtml(data.salesman)}</td> </tr> <tr> <td class="cell-title">å¶åæ¥æ:</td> <td class="left">${escapeHtml(formatDisplayDate(data.executionDate || data.orderMakerDate || data.registerDate || data.entryDate))}</td> <td class="cell-title">äº¤è´§æ¥æ:</td> <td class="left">${escapeHtml(formatDisplayDate(data.deliveryDate))}</td> <td class="left"></td> </tr> <tr> <td class="cell-title">éè´§å°å:</td> <td colspan="4" class="left">${escapeHtml(getDeliveryAddress(data))}</td> </tr> </table> <div class="detail-table-wrap"> <table class="detail-table"> <colgroup> <col style="width: 9%;" /> <col style="width: 12%;" /> <col style="width: 12%;" /> <col style="width: 7%;" /> <col style="width: 10%;" /> <col style="width: 8%;" /> <col style="width: 10%;" /> <col style="width: 32%;" /> </colgroup> <tr> <th>åºå·</th> <th>楼å±ç¼å·</th> <th>宽(å¼§é¿)*é«</th> <th>æ°é</th> <th>ç»ç®é¢ç§¯</th> <th>åä»·</th> <th>éé¢</th> <th>å å·¥è¦æ±</th> </tr> <tr class="product-row"> <td colspan="6" class="left">产ååç§°: ${escapeHtml(data.productName || items[0]?.productDescription)}</td> <td colspan="2" class="left">订åç¼å·: ${escapeHtml(data.salesContractNo)}</td> </tr> ${renderRows(pageItems, startIndex)} ${ isLastPage ? ` <tr class="total-row"> <td colspan="3" class="left">å°è®¡:</td> <td>${subtotalQuantity || totalQuantity || ""}</td> <td>${subtotalArea ? subtotalArea.toFixed(2) : totalArea ? totalArea.toFixed(2) : ""}</td> <td></td> <td>${formatMoney(subtotalAmount || totalAmount)}</td> <td></td> </tr> <tr class="total-row"> <td colspan="3" class="left">å计:</td> <td>${totalQuantity || ""}</td> <td>${totalArea ? totalArea.toFixed(2) : ""}</td> <td></td> <td>${formatMoney(totalAmount)}</td> <td></td> </tr> <tr class="other-fee-header-row"> <td colspan="5" class="left other-fee-header"> <div class="other-fee-header-grid"> <span>å ¶ä»è´¹ç¨</span> <span>åä»·</span> <span>æ°é</span> <span>éé¢</span> </div> </td> <td colspan="3" class="left other-fee-header">å å·¥è¦æ±å夿³¨:</td> </tr> <tr class="large-row other-fee-content-row"> <td colspan="5" class="left other-fee-cell"> <table class="other-fee-inner"> <colgroup> <col style="width: 34%;" /> <col style="width: 22%;" /> <col style="width: 20%;" /> <col style="width: 24%;" /> </colgroup> ${renderOtherFeeRows(otherFees)} </table> </td> <td colspan="3" class="left other-fee-cell">${escapeHtml(normalizeRequirementText(data.remakes || data.remarks || data.orderProcessRequirement))}</td> </tr> <tr class="total-row"> <td colspan="8" class="left">æ»éé¢: ${escapeHtml(data.totalAmountDisplay || `${formatMoney(totalAmount)}å `)}</td> </tr> ` : ` <tr><td colspan="8" class="right">ä¸é¡µç»...</td></tr> ` } </table> </div> </div> ${ isLastPage ? ` <div class="customer-sign-row"> <span></span> <span></span> <span class="customer-sign customer-sign-cell">客æ·ç¾å:</span> </div> <div class="footer"> <div><b>å¶åå:</b>${escapeHtml(data.orderMaker || data.register)}</div> <div><b>å®¡æ ¸å:</b>${escapeHtml(data.auditor)}</div> <div><b>æå°äºº:</b>${escapeHtml(data.printPeople || data.register)}</div> <div><b>å¶åæ¥æ:</b>${escapeHtml(formatDisplayDate(data.orderMakerDate || data.executionDate || data.registerDate || data.entryDate))}</div> <div><b>å®¡æ ¸æ¥æ:</b>${escapeHtml(formatDisplayDate(data.auditDate))}</div> <div class="footer-pair"><span><b>æå°æ¶é´:</b>${escapeHtml(data.printTime || getCurrentDateTime())}</span><span class="footer-page-right">第${pageIndex + 1}页,å ±${totalPages}页</span></div> </div> ` : "" } </div>`; }) .join("")} </body> </html> `; printWindow.document.write(html); printWindow.document.close(); printWindow.onload = () => { setTimeout(() => { printWindow.focus(); printWindow.print(); printWindow.close(); }, 300); }; }; src/views/salesManagement/salesLedger/index.vue
@@ -25,18 +25,39 @@ </div> <div class="table_list"> <div class="actions"> <div></div> <div> <OtherAmountMaintenanceButton /> <ProcessFlowMaintenanceButton /> </div> <ProcessFlowConfigSelectDialog v-model:visible="processFlowSelectDialogVisible" :default-route-id="processFlowSelectDefaultRouteId" :bound-route-name="processFlowSelectBoundRouteName" @confirm="handleProcessFlowSelectConfirm" /> <div> <el-button type="primary" @click="openForm('add')"> æ°å¢å°è´¦ </el-button> <el-button type="primary" plain @click="openOtherAmountDialog"> å ¶ä»éé¢ç»´æ¤ <el-button type="primary" @click="handleBulkDelivery"> åè´§ </el-button> <el-button type="primary" plain @click="handleImport">å¯¼å ¥</el-button> <el-button @click="handleOut">导åº</el-button> <el-button type="danger" plain @click="handleDelete">å é¤</el-button> <el-button type="primary" plain @click="handlePrint">æå°</el-button> <el-dropdown @command="handlePrintCommand"> <el-button type="primary" plain> æå°åæ®<el-icon class="el-icon--right"><ArrowDown /></el-icon> </el-button> <template #dropdown> <el-dropdown-menu> <el-dropdown-item command="finishedProcessCard">ç产æµç¨å¡ï¼æåï¼</el-dropdown-item> <el-dropdown-item command="salesOrder">éå®è®¢å</el-dropdown-item> <el-dropdown-item command="salesDeliveryNote">éå®åè´§å</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> <el-button type="primary" plain @click="handlePrintLabel">æå°æ ç¾</el-button> </div> </div> <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange" @@ -49,7 +70,11 @@ <el-table-column align="center" label="åºå·" type="index"/> <el-table-column label="产å大类" prop="productCategory" /> <el-table-column label="è§æ ¼åå·" prop="specificationModel" /> <el-table-column label="åä½" prop="unit" /> <el-table-column label="å度" prop="thickness" min-width="90"> <template #default="scope"> {{ scope.row.thickness ?? "" }} </template> </el-table-column> <el-table-column label="产åç¶æ" width="100px" align="center"> @@ -96,7 +121,7 @@ <el-table-column label="å«ç¨æ»ä»·(å )" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" /> <el-table-column label="ä¸å«ç¨æ»ä»·(å )" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" /> <!--æä½--> <el-table-column Width="60px" label="æä½" align="center"> <!-- <el-table-column Width="60px" label="æä½" align="center"> <template #default="scope"> <el-button link @@ -106,7 +131,7 @@ åè´§ </el-button> </template> </el-table-column> </el-table-column> --> </el-table> </template> </el-table-column> @@ -123,9 +148,10 @@ <el-table-column label="ç¾è®¢æ¥æ" prop="executionDate" width="120" show-overflow-tooltip /> <el-table-column label="äº¤ä»æ¥æ" prop="deliveryDate" width="120" show-overflow-tooltip /> <el-table-column label="夿³¨" prop="remarks" width="200" show-overflow-tooltip /> <el-table-column fixed="right" label="æä½" width="130" align="center"> <el-table-column fixed="right" label="æä½" width="200" align="center"> <template #default="scope"> <el-button link type="primary" @click="openForm('edit', scope.row)" :disabled="!scope.row.isEdit">ç¼è¾</el-button> <el-button link type="primary" @click="openProcessFlowSelect(scope.row)" :disabled="!scope.row.isEdit">å·¥èºè·¯çº¿</el-button> <el-button link type="primary" @click="downLoadFile(scope.row)">éä»¶</el-button> </template> </el-table-column> @@ -229,7 +255,11 @@ <el-table-column align="center" label="åºå·" type="index" width="60" /> <el-table-column label="产å大类" prop="productCategory" /> <el-table-column label="è§æ ¼åå·" prop="specificationModel" /> <el-table-column label="åä½" prop="unit" /> <el-table-column label="å度" prop="thickness" min-width="90"> <template #default="scope"> {{ scope.row.thickness ?? "" }} </template> </el-table-column> <el-table-column label="æ°é" prop="quantity" /> <el-table-column label="ç¨ç(%)" prop="taxRate" /> <el-table-column label="å«ç¨åä»·(å )" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" /> @@ -247,6 +277,20 @@ <el-col :span="24"> <el-form-item label="夿³¨ï¼" prop="remarks"> <el-input v-model="form.remarks" placeholder="请è¾å ¥" clearable type="textarea" :rows="2" :disabled="operationType === 'view'" /> </el-form-item> </el-col> </el-row> <el-row :gutter="30"> <el-col :span="24"> <el-form-item label="客æ·å¤æ³¨ï¼" prop="customerRemarks"> <el-input v-model="form.customerRemarks" placeholder="请è¾å ¥" clearable type="textarea" :rows="2" :disabled="operationType === 'view'" /> </el-form-item> </el-col> </el-row> @@ -374,8 +418,16 @@ </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="åä½ï¼" prop="unit"> <el-input v-model="productForm.unit" placeholder="请è¾å ¥" clearable /> <el-form-item label="å度ï¼" prop="thickness"> <el-input-number v-model="productForm.thickness" :min="0" :step="0.000000000000001" :precision="15" style="width: 100%;" placeholder="请è¾å ¥" clearable /> </el-form-item> </el-col> </el-row> @@ -564,6 +616,13 @@ </el-form-item> </el-col> </el-row> <el-row :gutter="30"> <el-col :span="24"> <el-form-item label="楼å±ç¼å·ï¼" prop="floorCode"> <el-input v-model="productForm.floorCode" placeholder="请è¾å ¥æ¥¼å±ç¼å·" clearable type="textarea" :rows="2" :disabled="operationType === 'view'" /> </el-form-item> </el-col> </el-row> <!-- å ¶ä»éé¢ï¼å 满ä¸è¡ï¼çåäº 3 åç½æ ¼çæ´è¡ï¼ --> <el-row :gutter="30"> <el-col :span="12"> @@ -684,6 +743,7 @@ </el-button> </template> </el-dialog> <!-- å¯¼å ¥å¼¹çª --> <FormDialog v-model="importUpload.open" @@ -725,122 +785,6 @@ v-model="fileListDialogVisible" title="éä»¶å表" /> <!-- æå°é¢è§å¼¹çª --> <el-dialog v-model="printPreviewVisible" title="æå°é¢è§" width="90%" :close-on-click-modal="false" class="print-preview-dialog" > <div class="print-preview-container"> <div class="print-preview-header"> <el-button type="primary" @click="executePrint">æ§è¡æå°</el-button> <el-button @click="printPreviewVisible = false">å ³éé¢è§</el-button> </div> <div class="print-preview-content"> <div v-if="printData.length === 0" style="text-align: center; padding: 50px; color: #999;"> ææ æå°æ°æ® </div> <div v-else style="text-align: center; padding: 10px; color: #666; font-size: 14px; background: #e8f4fd; margin-bottom: 10px;"> å ± {{ printData.length }} æ¡æ°æ®å¾ æå° </div> <div v-for="(item, index) in printData" :key="index" class="print-page"> <div class="delivery-note"> <div class="header"> <div class="document-title">é¶å®åè´§å</div> </div> <div class="info-section"> <div class="info-row"> <div> <span class="label">åè´§æ¥æï¼</span> <span class="value">{{ formatDate(item.createTime) }}</span> </div> <div> <span class="label">å货车çå·ï¼</span> <span class="value">{{ item.shippingCarNumber }}</span> </div> </div> <div class="info-row"> <div> <span class="label">客æ·åç§°ï¼</span> <span class="value">{{ item.customerName }}</span> </div> <span class="label">åå·ï¼</span> <span class="value">{{ item.salesContractNo }}</span> </div> </div> <div class="table-section"> <table class="product-table"> <thead> <tr> <th>产ååç§°</th> <th>è§æ ¼åå·</th> <th>åä½</th> <th>åä»·</th> <th>é¶å®æ°é</th> <th>é¶å®éé¢</th> </tr> </thead> <tbody> <tr v-for="product in item.products" :key="product.id"> <td>{{ product.productCategory || '' }}</td> <td>{{ product.specificationModel || '' }}</td> <td>{{ product.unit || '' }}</td> <td>{{ product.taxInclusiveUnitPrice || '0' }}</td> <td>{{ product.quantity || '0' }}</td> <td>{{ product.taxInclusiveTotalPrice || '0' }}</td> </tr> <tr v-if="!item.products || item.products.length === 0"> <td colspan="6" style="text-align: center; color: #999;">ææ äº§åæ°æ®</td> </tr> </tbody> <tfoot> <tr> <td class="label">å计</td> <td class="total-value"></td> <td class="total-value"></td> <td class="total-value"></td> <td class="total-value">{{ getTotalQuantity(item.products) }}</td> <td class="total-value">{{ getTotalAmount(item.products) }}</td> </tr> </tfoot> </table> </div> <div class="footer-section"> <div class="footer-row"> <div class="footer-item"> <span class="label">æ¶è´§çµè¯ï¼</span> <span class="value"></span> </div> <div class="footer-item"> <span class="label">æ¶è´§äººï¼</span> <span class="value"></span> </div> <div class="footer-item address-item"> <span class="label">æ¶è´§å°åï¼</span> <span class="value address-value"></span> </div> </div> <div class="footer-row"> <div class="footer-item"> <span class="label">æä½åï¼</span> <span class="value">{{ userStore.nickName || 'æå¼å' }}</span> </div> <div class="footer-item"> <span class="label">æå°æ¥æï¼</span> <span class="value">{{ formatDateTime(new Date()) }}</span> </div> </div> </div> </div> </div> </div> </div> </el-dialog> <!-- åè´§å¼¹æ¡ --> <el-dialog v-model="deliveryFormVisible" @@ -916,114 +860,6 @@ </template> </el-dialog> <!-- å ¶ä»éé¢ç»´æ¤ï¼æ°å¢/ç¼è¾/å é¤ï¼ --> <el-dialog v-model="otherAmountDialogVisible" title="å ¶ä»éé¢ç»´æ¤" width="80%" :close-on-click-modal="false" @close="closeOtherAmountDialog" > <el-row :gutter="20"> <el-col :span="14"> <el-table :data="otherAmountRecords" border v-loading="otherAmountLoading" height="55vh" > <el-table-column label="ç¼ç " prop="code" min-width="120" show-overflow-tooltip /> <el-table-column label="项ç®" prop="processName" min-width="180" show-overflow-tooltip /> <el-table-column label="æ°é" prop="quantity" min-width="110" :formatter="formattedNumber" /> <el-table-column label="åä»·(å )" prop="unitPrice" min-width="130" :formatter="formattedNumber" /> <el-table-column label="éé¢(å )" prop="amount" min-width="160" :formatter="formattedNumber" /> <el-table-column fixed="right" label="æä½" width="160" align="center"> <template #default="scope"> <el-button link type="primary" size="small" @click="handleOtherEdit(scope.row)">ç¼è¾</el-button> <el-button link type="danger" size="small" @click="handleOtherDelete(scope.row)">å é¤</el-button> </template> </el-table-column> </el-table> <pagination v-show="otherAmountTotal > 0" :total="otherAmountTotal" layout="total, sizes, prev, pager, next, jumper" :page="otherAmountPage.current" :limit="otherAmountPage.size" @pagination="otherAmountPaginationChange" /> </el-col> <el-col :span="10"> <div style="padding: 8px 0;"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 10px;"> <div style="font-weight:600;"> {{ otherAmountOperationType === 'add' ? 'æ°å¢å ¶ä»éé¢' : 'ç¼è¾å ¶ä»éé¢' }} </div> <el-button type="primary" plain size="small" @click="handleOtherAdd" :disabled="otherAmountOperationType === 'add'" > æ°å¢ </el-button> </div> <el-form :model="otherAmountForm" label-width="120px" label-position="top" :rules="otherAmountRules" ref="otherAmountFormRef" > <el-form-item label="ç¼ç "> <el-input v-model="otherAmountForm.code" placeholder="请è¾å ¥ç¼ç ï¼å¯éï¼" clearable /> </el-form-item> <el-form-item label="项ç®" prop="processName"> <el-input v-model="otherAmountForm.processName" placeholder="请è¾å ¥é¡¹ç®åç§°" clearable /> </el-form-item> <el-form-item label="æ°é" prop="quantity"> <el-input-number v-model="otherAmountForm.quantity" :min="0" :precision="2" style="width:100%" placeholder="请è¾å ¥æ°é" clearable @change="recalcOtherAmount" /> </el-form-item> <el-form-item label="åä»·(å )" prop="unitPrice"> <el-input-number v-model="otherAmountForm.unitPrice" :min="0" :precision="2" style="width:100%" placeholder="请è¾å ¥åä»·" clearable @change="recalcOtherAmount" /> </el-form-item> <el-form-item label="éé¢(å )"> <el-input v-model="otherAmountForm.amount" disabled /> </el-form-item> <div style="display:flex; justify-content:flex-end; gap: 10px; margin-top: 8px;"> <el-button @click="closeOtherAmountDialog">åæ¶</el-button> <el-button type="primary" @click="submitOtherAmountForm">ä¿å</el-button> </div> </el-form> </div> </el-col> </el-row> </el-dialog> </div> </template> @@ -1033,11 +869,14 @@ import {onMounted, ref, getCurrentInstance} from "vue"; import { addShippingInfo } from "@/api/salesManagement/deliveryLedger.js"; import { ElMessageBox, ElMessage } from "element-plus"; import { UploadFilled, Download } from "@element-plus/icons-vue"; import { ArrowDown } from "@element-plus/icons-vue"; import useUserStore from "@/store/modules/user"; import { userListNoPage } from "@/api/system/user.js"; import FileListDialog from '@/components/Dialog/FileListDialog.vue'; import FormDialog from '@/components/Dialog/FormDialog.vue'; import OtherAmountMaintenanceButton from "./components/OtherAmountMaintenanceButton.vue"; import ProcessFlowMaintenanceButton from "./components/ProcessFlowMaintenanceButton.vue"; import ProcessFlowConfigSelectDialog from "./components/ProcessFlowConfigSelectDialog.vue"; import { getQuotationList } from "@/api/salesManagement/salesQuotation.js"; import { ledgerListPage, @@ -1051,14 +890,22 @@ delLedgerFile, getProductInventory, salesLedgerProductProcessList, salesLedgerProductProcessAdd, salesLedgerProductProcessUpdate, salesLedgerProductProcessDelete, saleProcessBind, getSaleProcessBindInfo, getProcessCard, getSalesOrder, getSalesInvoices, getSalesLabel, } from "@/api/salesManagement/salesLedger.js"; import { modelList, productTreeList } from "@/api/basicData/product.js"; import useFormData from "@/hooks/useFormData.js"; import dayjs from "dayjs"; import { getCurrentDate } from "@/utils/index.js"; import { printFinishedProcessCard } from "./components/processCardPrint.js"; import { printSalesOrder } from "./components/salesOrderPrint.js"; import { printSalesDeliveryNote } from "./components/salesDeliveryPrint.js"; import { printSalesLabel } from "./components/salesLabelPrint.js"; // import { salesLedgerProductSetProcessFlowConfig } from "@/api/salesManagement/salesProcessFlowConfig.js"; const userStore = useUserStore(); const { proxy } = getCurrentInstance(); @@ -1077,6 +924,13 @@ }); const total = ref(0); const fileList = ref([]); // å·¥èºè·¯çº¿é ç½®éæ©å¼¹çªï¼ç»å®å°å°è´¦äº§åï¼ const processFlowSelectDialogVisible = ref(false); const processFlowSelectLedgerRow = ref(null); const processFlowSelectDefaultRouteId = ref(null); const processFlowSelectBoundRouteId = ref(null); const processFlowSelectBoundRouteName = ref(""); // ç¨æ·ä¿¡æ¯è¡¨åå¼¹æ¡æ°æ® const operationType = ref(""); @@ -1119,7 +973,7 @@ productForm: { productCategory: "", specificationModel: "", unit: "", thickness:null, quantity: "", taxInclusiveUnitPrice: "", taxRate: "", @@ -1138,6 +992,8 @@ processRequirement: "", // å å·¥è¦æ± remark: "", // 夿³¨ salesProductProcessList: [], // å ¶ä»éé¢ï¼[{id, processName, quantity}] processFlowConfigId: null, // å·¥èºæµç¨é ç½®ç»å® floorCode: "", // 楼å±ç¼å· }, productRules: { productCategory: [{ required: true, message: "è¯·éæ©", trigger: "change" }], @@ -1145,7 +1001,7 @@ specificationModel: [ { required: true, message: "è¯·éæ©", trigger: "change" }, ], unit: [{ required: true, message: "请è¾å ¥", trigger: "blur" }], thickness: [{ required: true, message: "请è¾å ¥", trigger: "blur" }], quantity: [{ required: true, message: "请è¾å ¥", trigger: "blur" }], taxInclusiveUnitPrice: [ { required: true, message: "请è¾å ¥", trigger: "blur" }, @@ -1169,10 +1025,6 @@ // 设置ä¸ä¼ ç请æ±å¤´é¨ headers: { Authorization: "Bearer " + getToken() }, }); // æå°ç¸å ³ const printPreviewVisible = ref(false); const printData = ref([]); // æ¥ä»·åå¯¼å ¥ç¸å ³ const quotationDialogVisible = ref(false); const quotationLoading = ref(false); @@ -1191,7 +1043,7 @@ // åè´§ç¸å ³ const deliveryFormVisible = ref(false); const currentDeliveryRow = ref(null); const currentDeliveryRows = ref([]); const deliveryFormData = reactive({ deliveryForm: { type: "货车", // 货车, å¿«é @@ -1203,169 +1055,6 @@ }, }); const { deliveryForm, deliveryRules } = toRefs(deliveryFormData); // å ¶ä»éé¢ç»´æ¤ï¼å·¥åº/æµç¨éé¢ç»´æ¤ï¼ const otherAmountDialogVisible = ref(false); const otherAmountLoading = ref(false); const otherAmountRecords = ref([]); const otherAmountTotal = ref(0); const otherAmountPage = reactive({ current: 1, size: 10, }); const otherAmountOperationType = ref("add"); // add/edit const otherAmountFormRef = ref(null); const otherAmountForm = reactive({ id: null, code: "", // åç«¯åæ®µåï¼codeï¼å端æ¥å£å表è¿å remarkï¼æ¤å¤è¿è¡æ å° processName: "", quantity: 0, unitPrice: 0, amount: "0.00", }); const otherAmountRules = reactive({ processName: [{ required: true, message: "请è¾å ¥é¡¹ç®åç§°", trigger: "change" }], quantity: [{ required: true, message: "请è¾å ¥æ°é", trigger: "blur" }], unitPrice: [{ required: true, message: "请è¾å ¥åä»·", trigger: "blur" }], }); const recalcOtherAmount = () => { const quantity = Number(otherAmountForm.quantity ?? 0) || 0; const unitPrice = Number(otherAmountForm.unitPrice ?? 0) || 0; otherAmountForm.amount = (quantity * unitPrice).toFixed(2); }; const resetOtherAmountForm = (type = "add") => { otherAmountOperationType.value = type; otherAmountForm.id = null; otherAmountForm.code = ""; otherAmountForm.processName = ""; otherAmountForm.quantity = 0; otherAmountForm.unitPrice = 0; otherAmountForm.amount = "0.00"; }; const openOtherAmountDialog = () => { otherAmountDialogVisible.value = true; resetOtherAmountForm("add"); // æå¼å¼¹æ¡æ¶å·æ°æ°æ®ï¼é¿å é¿æ¶é´åçå¯¼è´æ°æ®è¿æ otherAmountPage.current = otherAmountPage.current || 1; fetchOtherAmountList(); }; const closeOtherAmountDialog = () => { otherAmountDialogVisible.value = false; resetOtherAmountForm("add"); }; const fetchOtherAmountList = async () => { otherAmountLoading.value = true; try { const params = { current: otherAmountPage.current, size: otherAmountPage.size, }; const res = await salesLedgerProductProcessList(params); // å ¼å®¹ä¸åæ¥å£ååºç»æï¼å¯è½æ¯ res.records / res.total æ res.data.records / res.data.total const records = res?.records ?? res?.data?.records ?? []; const total = res?.total ?? res?.data?.total ?? 0; otherAmountRecords.value = records.map((item) => { const quantity = Number(item.quantity ?? 0) || 0; const unitPrice = Number(item.unitPrice ?? 0) || 0; const amount = Number(item.amount ?? quantity * unitPrice) || 0; return { id: item.id, code: item.code ?? item.remark ?? "", processName: item.processName ?? "", quantity, unitPrice, amount: amount.toFixed(2), }; }); otherAmountTotal.value = total; } finally { otherAmountLoading.value = false; } }; const otherAmountPaginationChange = (obj) => { otherAmountPage.current = obj.page; otherAmountPage.size = obj.limit; fetchOtherAmountList(); }; const handleOtherAdd = () => { resetOtherAmountForm("add"); }; const handleOtherEdit = (row) => { if (!row) return; otherAmountOperationType.value = "edit"; otherAmountForm.id = row.id ?? null; otherAmountForm.code = row.code ?? ""; otherAmountForm.processName = row.processName ?? ""; otherAmountForm.quantity = Number(row.quantity ?? 0) || 0; otherAmountForm.unitPrice = Number(row.unitPrice ?? 0) || 0; recalcOtherAmount(); }; const submitOtherAmountForm = () => { otherAmountFormRef.value?.validate((valid) => { if (!valid) return; const payload = { processName: otherAmountForm.processName, quantity: Number(otherAmountForm.quantity) || 0, unitPrice: Number(otherAmountForm.unitPrice) || 0, amount: Number(otherAmountForm.amount) || 0, // å表è¿ååæ®µæ¯ remarkï¼è¿éæâcode=remarkâåæ å° remark: otherAmountForm.code, // å ¼å®¹å端å¯è½ç´æ¥ä½¿ç¨ code åæ®µ code: otherAmountForm.code, }; if (otherAmountOperationType.value === "edit") { payload.id = otherAmountForm.id; salesLedgerProductProcessUpdate(payload).then(() => { proxy.$modal.msgSuccess("ä¿åæå"); fetchOtherAmountList(); resetOtherAmountForm("add"); }); } else { salesLedgerProductProcessAdd(payload).then(() => { proxy.$modal.msgSuccess("ä¿åæå"); fetchOtherAmountList(); resetOtherAmountForm("add"); }); } }); }; const handleOtherDelete = (row) => { if (!row?.id) return; ElMessageBox.confirm("确认å é¤è¯¥è®°å½ï¼", "å é¤", { confirmButtonText: "确认", cancelButtonText: "åæ¶", type: "warning", }) .then(() => { return salesLedgerProductProcessDelete(row.id).then(() => { proxy.$modal.msgSuccess("å 餿å"); fetchOtherAmountList(); if (otherAmountOperationType.value === "edit" && otherAmountForm.id === row.id) { resetOtherAmountForm("add"); } }); }) .catch(() => { proxy.$modal.msg("已忶"); }); }; // 产åå¼¹æ¡ï¼å ¶ä»éé¢å¤é䏿ï¼åºäºâå ¶ä»éé¢ç»´æ¤âæ¥è¯¢æ¥å£ï¼ const otherAmountSelectOptions = ref([]); // [{id, processName}] @@ -1636,6 +1325,86 @@ tableLoading.value = false; }); }; // æå¼âå·¥èºè·¯çº¿é ç½®â鿩弹çªï¼å¿ é¡»æ¾å¼éæ©ï¼ const openProcessFlowSelect = async (ledgerRow) => { if (!ledgerRow) return; if (!ledgerRow.isEdit) return; processFlowSelectLedgerRow.value = ledgerRow; processFlowSelectDefaultRouteId.value = null; processFlowSelectBoundRouteId.value = null; processFlowSelectBoundRouteName.value = ""; try { const res = await getSaleProcessBindInfo(ledgerRow.id); const info = res?.data ?? res ?? {}; const boundId = info?.processRouteId ?? info?.routeId ?? info?.id ?? null; const boundName = info?.processRouteName ?? info?.routeName ?? info?.name ?? ""; processFlowSelectBoundRouteId.value = boundId; processFlowSelectBoundRouteName.value = boundName; processFlowSelectDefaultRouteId.value = boundId; } catch (e) { // æ¥è¯¢å¤±è´¥æ¶ææªç»å®å¤çï¼ä¸é»å¡å¼¹çª processFlowSelectBoundRouteId.value = null; processFlowSelectBoundRouteName.value = ""; processFlowSelectDefaultRouteId.value = null; } processFlowSelectDialogVisible.value = true; }; // ç»å®å·¥èºè·¯çº¿å°å½åå°è´¦æ°æ® const handleProcessFlowSelectConfirm = async (routeId) => { const ledgerRow = processFlowSelectLedgerRow.value; if (!ledgerRow?.id) return; const finalRouteId = routeId ?? null; if (!finalRouteId) return; const oldRouteId = processFlowSelectBoundRouteId.value; if (oldRouteId !== null && oldRouteId !== undefined && oldRouteId !== "" && String(oldRouteId) !== String(finalRouteId)) { try { await ElMessageBox.confirm( "该订åå·²ç»å®å·¥èºè·¯çº¿ï¼æ¯å¦ç¡®å®æ´æ¢ï¼", "æç¤º", { confirmButtonText: "ç¡®å®", cancelButtonText: "åæ¶", type: "warning", } ); } catch { return; } } proxy?.$modal?.loading?.("æ£å¨ç»å®å·¥èºè·¯çº¿ï¼è¯·ç¨å..."); try { await saleProcessBind({ salesLedgerId: ledgerRow.id, processRouteId: finalRouteId, }); proxy?.$modal?.msgSuccess?.("å·¥èºè·¯çº¿ç»å®æå"); processFlowSelectDialogVisible.value = false; // ç»å®åå·æ°å表ï¼ç¡®ä¿æä½å忬¡ç¹å»è½åæ¾ç»å® await getList(); } catch (e) { proxy?.$modal?.msgError?.("ç»å®å¤±è´¥ï¼è¯·ç¨åéè¯"); } finally { proxy?.$modal?.closeLoading?.(); } }; // è·å产å大类treeæ°æ® const getProductOptions = () => { // è¿å Promiseï¼ä¾¿äºå¨ç¼è¾äº§åæ¶çå¾ å è½½å®æ @@ -1658,10 +1427,8 @@ const index = modelOptions.value.findIndex((item) => item.id === value); if (index !== -1) { productForm.value.specificationModel = modelOptions.value[index].model; productForm.value.unit = modelOptions.value[index].unit; } else { productForm.value.specificationModel = null; productForm.value.unit = null; } }; const findNodeById = (nodes, productId) => { @@ -1784,11 +1551,14 @@ form.value.entryDate = getCurrentDate(); // ç¾è®¢æ¥æé»è®¤ä¸ºå½å¤© form.value.executionDate = getCurrentDate(); form.value.customerRemarks = ""; } else { currentId.value = row.id; getSalesLedgerWithProducts({ id: row.id, type: 1 }).then((res) => { form.value = { ...res }; form.value.entryPerson = Number(res.entryPerson); // åæ®µåå ¼å®¹ï¼å端å¯è½è¿å customer_remarks form.value.customerRemarks = res?.customerRemarks ?? res?.customer_remarks ?? ""; productData.value = form.value.productData; fileList.value = form.value.salesLedgerFiles; }); @@ -1886,7 +1656,7 @@ // å°è´¦å段 productCategory: p.product || p.productName || "", specificationModel: p.specification || "", unit: p.unit || "", thickness: p.thickness, quantity: quantity, taxRate: taxRate, taxInclusiveUnitPrice: unitPrice.toFixed(2), @@ -1901,6 +1671,7 @@ settlePieceArea: 0, settleTotalArea: 0, processRequirement: "", floorCode: "", remark: "", salesProductProcessList: [], }; @@ -2007,10 +1778,17 @@ productForm.value.processRequirement = row?.processRequirement ?? row?.process_requirement ?? ""; productForm.value.remark = row?.remark ?? row?.remarks ?? ""; productForm.value.floorCode = row?.floorCode ?? row?.floor_code ?? ""; // å·¥èºæµç¨é ç½®ç»å®å段ï¼åç»ç±åç«¯ç¡®è®¤åæ®µåï¼ productForm.value.processFlowConfigId = row?.processFlowConfigId ?? row?.process_flow_config_id ?? null; // å¨é¿åæ¾ï¼å¦å端è¿åï¼æç»ä»ä»¥å ¬å¼è®¡ç®ä¸ºåï¼ productForm.value.perimeter = row?.perimeter ?? row?.heavyBoxPerimeter ?? row?.heavyboxPerimeter ?? 0; // åç«¯ç´æ¥è¿å thickness productForm.value.thickness = row?.thickness; productForm.value.salesProductProcessList = normalizeOtherAmountsFromRow(row); productIndex.value = index; @@ -2055,6 +1833,11 @@ const submitProduct = () => { proxy.$refs["productFormRef"].validate((valid) => { if (valid) { // å度ä¿ç 15 ä½å°æ°ï¼é¿å ç±äºæµ®ç¹è®¡ç®/è¾å ¥å¯¼è´ç²¾åº¦åå·® if (productForm.value.thickness !== null && productForm.value.thickness !== undefined) { productForm.value.thickness = Number(Number(productForm.value.thickness).toFixed(15)); } // é¢ç§¯/æ»è®¡åæ®µå¨æäº¤åå åºè®¡ç®ä¸æ¬¡ recalcAreaTotals(); // å ¶ä»éé¢åªæäº¤ {id, processName, quantity}ï¼åç«¯åæ®µï¼salesProductProcessListï¼ @@ -2248,364 +2031,118 @@ }); }; // æå°åè½ const handlePrint = async () => { if (selectedRows.value.length === 0) { proxy.$modal.msgWarning("è¯·éæ©è¦æå°çæ°æ®"); const handlePrintCommand = async (command) => { if (command !== "finishedProcessCard" && command !== "salesOrder" && command !== "salesDeliveryNote") return; if (command === "salesDeliveryNote") { if (selectedRows.value.length === 0) { proxy.$modal.msgWarning("请è³å°éæ©ä¸æ¡éå®å°è´¦æ°æ®è¿è¡æå°"); return; } const customerNames = Array.from( new Set(selectedRows.value.map((item) => String(item?.customerName ?? "").trim())) ); if (customerNames.length > 1) { proxy.$modal.msgWarning("ä» æ¯æç¸å客æ·åç§°çéå®å°è´¦åå¹¶åè´§æå°"); return; } } else if (selectedRows.value.length !== 1) { proxy.$modal.msgWarning("è¯·éæ©ä¸æ¡éå®å°è´¦æ°æ®è¿è¡æå°"); return; } // æ¾ç¤ºå è½½ç¶æ proxy.$modal.loading("æ£å¨è·åäº§åæ°æ®ï¼è¯·ç¨å..."); try { // 为æ¯ä¸ªéä¸çéå®å°è´¦è®°å½æ¥è¯¢å¯¹åºçäº§åæ°æ® const printDataWithProducts = []; for (const row of selectedRows.value) { try { // è°ç¨productListæ¥å£æ¥è¯¢äº§åæ°æ® const productRes = await productList({ salesLedgerId: row.id, type: 1 }); // å°äº§åæ°æ®æ´åå°éå®å°è´¦è®°å½ä¸ const rowWithProducts = { ...row, products: productRes.data || [] }; printDataWithProducts.push(rowWithProducts); } catch (error) { console.error(`è·åéå®å°è´¦ ${row.id} çäº§åæ°æ®å¤±è´¥:`, error); // å³ä½¿æä¸ªè®°å½çäº§åæ°æ®è·å失败ï¼ä¹è¦å å«è¯¥è®°å½ printDataWithProducts.push({ ...row, products: [] }); } const selectedRow = selectedRows.value[0]; const selectedId = selectedRow?.id; if (command === "salesDeliveryNote") { const selectedIds = selectedRows.value .map((item) => item?.id) .filter((id) => id !== null && id !== undefined && id !== ""); if (selectedIds.length !== selectedRows.value.length) { proxy.$modal.msgWarning("å½åéæ©æ°æ®åå¨ç¼ºå°IDçè®°å½ï¼æ æ³æå°"); return; } printData.value = printDataWithProducts; console.log('æå°æ°æ®ï¼å å«äº§åï¼:', printData.value); printPreviewVisible.value = true; const loadingText = command === "salesOrder" ? "æ£å¨è·åéå®è®¢åæ°æ®ï¼è¯·ç¨å..." : command === "salesDeliveryNote" ? "æ£å¨è·åéå®åè´§åæ°æ®ï¼è¯·ç¨å..." : "æ£å¨è·åç产æµç¨å¡æ°æ®ï¼è¯·ç¨å..."; proxy.$modal.loading(loadingText); try { const res = await getSalesInvoices(selectedIds); const salesInvoiceData = res?.data ?? {}; printSalesDeliveryNote(salesInvoiceData, selectedRow); } catch (error) { console.error("æå°éå®åè´§å失败:", error); proxy.$modal.msgError("æå°å¤±è´¥ï¼è¯·ç¨åéè¯"); } finally { proxy.$modal.closeLoading(); } return; } if (!selectedId) { proxy.$modal.msgWarning("å½åéæ©æ°æ®ç¼ºå°IDï¼æ æ³æå°"); return; } const loadingText = command === "salesOrder" ? "æ£å¨è·åéå®è®¢åæ°æ®ï¼è¯·ç¨å..." : command === "salesDeliveryNote" ? "æ£å¨è·åéå®åè´§åæ°æ®ï¼è¯·ç¨å..." : "æ£å¨è·åç产æµç¨å¡æ°æ®ï¼è¯·ç¨å..."; proxy.$modal.loading(loadingText); try { if (command === "salesOrder") { const res = await getSalesOrder(selectedId); const salesOrderData = res?.data ?? {}; printSalesOrder(salesOrderData); } else { const res = await getProcessCard(selectedId); const processCardData = res?.data ?? {}; printFinishedProcessCard(processCardData); } } catch (error) { console.error('è·åäº§åæ°æ®å¤±è´¥:', error); proxy.$modal.msgError("è·åäº§åæ°æ®å¤±è´¥ï¼è¯·éè¯"); console.error( command === "salesOrder" ? "æå°éå®è®¢å失败:" : command === "salesDeliveryNote" ? "æå°éå®åè´§å失败:" : "æå°ç产æµç¨å¡å¤±è´¥:", error ); proxy.$modal.msgError("æå°å¤±è´¥ï¼è¯·ç¨åéè¯"); } finally { proxy.$modal.closeLoading(); } }; // æ§è¡æå° const executePrint = () => { console.log('å¼å§æ§è¡æå°ï¼æ°æ®æ¡æ°:', printData.value.length); console.log('æå°æ°æ®:', printData.value); // å建ä¸ä¸ªæ°çæå°çªå£ const printWindow = window.open('', '_blank', 'width=800,height=600'); // æå»ºæå°å 容 let printContent = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>æå°é¢è§</title> <style> body { margin: 0; padding: 0; font-family: "SimSun", serif; background: white; } .print-page { width: 200mm; height: 75mm; padding: 10mm; padding-left: 20mm; background: white; box-sizing: border-box; page-break-after: always; page-break-inside: avoid; } .print-page:last-child { page-break-after: avoid; } .delivery-note { width: 100%; height: 100%; font-size: 12px; line-height: 1.2; display: flex; flex-direction: column; color: #000; } .header { text-align: center; margin-bottom: 8px; } .company-name { font-size: 18px; font-weight: bold; margin-bottom: 4px; } .document-title { font-size: 16px; font-weight: bold; } .info-section { margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; } .info-row { line-height: 20px; } .label { font-weight: bold; width: 60px; font-size: 12px; } .value { margin-right: 20px; min-width: 80px; font-size: 12px; } .table-section { margin-bottom: 40px; // flex: 0.6; } .product-table { width: 100%; border-collapse: collapse; border: 1px solid #000; } .product-table th, .product-table td { border: 1px solid #000; padding: 6px; text-align: center; font-size: 12px; line-height: 1.4; } .product-table th { font-weight: bold; } .total-value { font-weight: bold; } .footer-section { margin-top: auto; } .footer-row { display: flex; margin-bottom: 3px; line-height: 22px; justify-content: space-between; } .footer-item { display: flex; margin-right: 20px; } .footer-item .label { font-weight: bold; width: 80px; font-size: 12px; } .footer-item .value { min-width: 80px; font-size: 12px; } .address-item .address-value { min-width: 200px; } @media print { body { margin: 0; padding: 0; } .print-page { margin: 0; padding: 10mm; /* padding-left: 20mm; */ page-break-inside: avoid; page-break-after: always; } .print-page:last-child { page-break-after: avoid; } } </style> </head> <body> `; // ä¸ºæ¯æ¡æ°æ®çææå°é¡µé¢ printData.value.forEach((item, index) => { printContent += ` <div class="print-page"> <div class="delivery-note"> <div class="header"> <div class="document-title">é¶å®åè´§å</div> </div> <div class="info-section"> <div class="info-row"> <div> <span class="label">åè´§æ¥æï¼</span> <span class="value">${formatDate(item.createTime)}</span> </div> <div> <span class="label">客æ·åç§°ï¼</span> <span class="value">${item.customerName}</span> </div> </div> <div class="info-row"> <span class="label">åå·ï¼</span> <span class="value">${item.salesContractNo || ''}</span> </div> </div> <div class="table-section"> <table class="product-table"> <thead> <tr> <th>产ååç§°</th> <th>è§æ ¼åå·</th> <th>åä½</th> <th>åä»·</th> <th>é¶å®æ°é</th> <th>é¶å®éé¢</th> </tr> </thead> <tbody> ${item.products && item.products.length > 0 ? item.products.map(product => ` <tr> <td>${product.productCategory || ''}</td> <td>${product.specificationModel || ''}</td> <td>${product.unit || ''}</td> <td>${product.taxInclusiveUnitPrice || '0'}</td> <td>${product.quantity || '0'}</td> <td>${product.taxInclusiveTotalPrice || '0'}</td> </tr> `).join('') : '<tr><td colspan="6" style="text-align: center; color: #999;">ææ äº§åæ°æ®</td></tr>' const handlePrintLabel = async () => { if (selectedRows.value.length !== 1) { proxy.$modal.msgWarning("è¯·éæ©ä¸æ¡éå®å°è´¦æ°æ®è¿è¡æ ç¾æå°"); return; } const selectedId = selectedRows.value[0]?.id; if (!selectedId) { proxy.$modal.msgWarning("å½åéæ©æ°æ®ç¼ºå°IDï¼æ æ³æå°æ ç¾"); return; } proxy.$modal.loading("æ£å¨è·åæ ç¾æ°æ®ï¼è¯·ç¨å..."); try { const res = await getSalesLabel(selectedId); const labelList = res?.data ?? []; if (!Array.isArray(labelList) || labelList.length === 0) { proxy.$modal.msgWarning("ææ å¯æå°æ ç¾æ°æ®"); return; } </tbody> <tfoot> <tr> <td class="label">å计</td> <td class="total-value"></td> <td class="total-value"></td> <td class="total-value"></td> <td class="total-value">${getTotalQuantityForPrint(item.products)}</td> <td class="total-value">${getTotalAmountForPrint(item.products)}</td> </tr> </tfoot> </table> </div> <div class="footer-section"> <div class="footer-row"> <div class="footer-item"> <span class="label">æ¶è´§çµè¯ï¼</span> <span class="value"></span> </div> <div class="footer-item"> <span class="label">æ¶è´§äººï¼</span> <span class="value"></span> </div> <div class="footer-item address-item"> <span class="label">æ¶è´§å°åï¼</span> <span class="value address-value"></span> </div> </div> <div class="footer-row"> <div class="footer-item"> <span class="label">æä½åï¼</span> <span class="value">${userStore.nickName || 'æå¼å'}</span> </div> <div class="footer-item"> <span class="label">æå°æ¥æï¼</span> <span class="value">${formatDateTime(new Date())}</span> </div> </div> </div> </div> </div> `; }); printContent += ` </body> </html> `; // åå ¥å 容尿°çªå£ printWindow.document.write(printContent); printWindow.document.close(); // çå¾ å 容å è½½å®æåæå° printWindow.onload = () => { setTimeout(() => { printWindow.print(); printWindow.close(); printPreviewVisible.value = false; }, 500); }; }; // æ ¼å¼åæ¥æ const formatDate = (dateString) => { if (!dateString) return getCurrentDate(); const date = new Date(dateString); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}/${month}/${day}`; }; // æ ¼å¼åæ¥ææ¶é´ const formatDateTime = (date) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); const seconds = String(date.getSeconds()).padStart(2, "0"); return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`; }; // 计ç®äº§åæ»æ°é const getTotalQuantity = (products) => { if (!products || products.length === 0) return '0'; const total = products.reduce((sum, product) => { return sum + (parseFloat(product.quantity) || 0); }, 0); return total.toFixed(2); }; // 计ç®äº§åæ»éé¢ const getTotalAmount = (products) => { if (!products || products.length === 0) return '0'; const total = products.reduce((sum, product) => { return sum + (parseFloat(product.taxInclusiveTotalPrice) || 0); }, 0); return total.toFixed(2); }; // ç¨äºæå°ç计ç®å½æ° const getTotalQuantityForPrint = (products) => { if (!products || products.length === 0) return '0'; const total = products.reduce((sum, product) => { return sum + (parseFloat(product.quantity) || 0); }, 0); return total.toFixed(2); }; const getTotalAmountForPrint = (products) => { if (!products || products.length === 0) return '0'; const total = products.reduce((sum, product) => { return sum + (parseFloat(product.taxInclusiveTotalPrice) || 0); }, 0); return total.toFixed(2); printSalesLabel(labelList); } catch (error) { console.error("æå°æ ç¾å¤±è´¥:", error); proxy.$modal.msgError("æå°æ ç¾å¤±è´¥ï¼è¯·ç¨åéè¯"); } finally { proxy.$modal.closeLoading(); } }; const mathNum = () => { @@ -2926,6 +2463,72 @@ return statusStr === 'å¾ åè´§' || statusStr === 'å®¡æ ¸æç»'; }; const handleBulkDelivery = async () => { if (selectedRows.value.length === 0) { proxy.$modal.msgWarning("è¯·éæ©æ°æ®"); return; } const customerNames = selectedRows.value.map((r) => String(r.customerName || "").trim()); const uniqueCustomers = Array.from(new Set(customerNames)); // 客æ·åç§°ä¸ä¸è´ä¸å 许åè´§ if (uniqueCustomers.length > 1) { proxy.$modal.msgWarning("客æ·åç§°ä¸ä¸è´ï¼ä¸å 许åè´§"); return; } // 夿¡ä¸å®¢æ·ä¸è´ï¼äºæ¬¡ç¡®è®¤ if (selectedRows.value.length > 1) { try { await ElMessageBox.confirm("æ¯å¦ç¡®è®¤åå¹¶åè´§ï¼", "åå¹¶åè´§", { confirmButtonText: "确认", cancelButtonText: "åæ¶", type: "warning", }); } catch (e) { proxy.$modal.msg("已忶"); return; } } proxy.$modal.loading("æ£å¨è·åäº§åæ°æ®ï¼è¯·ç¨å..."); try { const targets = []; for (const ledger of selectedRows.value) { let products = []; try { const res = await productList({ salesLedgerId: ledger.id, type: 1 }); products = res?.data || []; } catch { products = []; } for (const product of products) { if (!canShip(product)) continue; targets.push({ ...product, salesLedgerId: product.salesLedgerId || ledger.id, }); } } if (targets.length === 0) { proxy.$modal.msgWarning("没æå¯åè´§çæ°æ®"); return; } currentDeliveryRows.value = targets; deliveryForm.value = { type: "货车" }; // é置审æ¹äººèç¹ï¼é»è®¤ä¸ä¸ªç©ºèç¹ï¼ approverNodes.value = [{ id: 1, userId: null }]; nextApproverId = 2; deliveryFormVisible.value = true; } finally { proxy.$modal.closeLoading(); } }; /** * ä¸è½½æä»¶ * @@ -2949,7 +2552,7 @@ return; } currentDeliveryRow.value = row; currentDeliveryRows.value = [row]; deliveryForm.value = { type: "货车", }; @@ -2972,13 +2575,28 @@ const approveUserIds = approverNodes.value.map(node => node.userId).join(","); // ä¿åå½åå±å¼çè¡IDï¼ä»¥ä¾¿åè´§åéæ°å è½½åè¡¨æ ¼æ°æ® const currentExpandedKeys = [...expandedRowKeys.value]; const salesLedgerId = currentDeliveryRow.value.salesLedgerId; addShippingInfo({ salesLedgerId: salesLedgerId, salesLedgerProductId: currentDeliveryRow.value.id, type: deliveryForm.value.type, approveUserIds, }) const targets = currentDeliveryRows.value || []; if (targets.length === 0) { proxy.$modal.msgWarning("æªéæ©å¯åè´§çæ°æ®"); return; } // 便¬¡åè´§ï¼é¿å å¹¶åä¸åºåæ£å/ç¶ææ´æ°äºç¸å½±åï¼ const run = async () => { for (const item of targets) { const salesLedgerId = item.salesLedgerId; if (!salesLedgerId) continue; await addShippingInfo({ salesLedgerId, salesLedgerProductId: item.id, type: deliveryForm.value.type, approveUserIds, }); } }; run() .then(() => { proxy.$modal.msgSuccess("åè´§æå"); closeDeliveryDia(); @@ -2986,8 +2604,7 @@ getList().then(() => { // 妿ä¹åæå±å¼çè¡ï¼éæ°å è½½è¿äºè¡çåè¡¨æ ¼æ°æ® if (currentExpandedKeys.length > 0) { // ä½¿ç¨ Promise.all å¹¶è¡å è½½ææå±å¼è¡çåè¡¨æ ¼æ°æ® const loadPromises = currentExpandedKeys.map(ledgerId => { const loadPromises = currentExpandedKeys.map((ledgerId) => { return productList({ salesLedgerId: ledgerId, type: 1 }).then((res) => { const index = tableData.value.findIndex((item) => item.id === ledgerId); if (index > -1) { @@ -2996,12 +2613,14 @@ }); }); Promise.all(loadPromises).then(() => { // æ¢å¤å±å¼ç¶æ expandedRowKeys.value = currentExpandedKeys; }); } }); }) .catch(() => { proxy.$modal.msgError("å货失败ï¼è¯·ç¨åéè¯"); }); } }); }; @@ -3010,7 +2629,7 @@ const closeDeliveryDia = () => { proxy.resetForm("deliveryFormRef"); deliveryFormVisible.value = false; currentDeliveryRow.value = null; currentDeliveryRows.value = []; }; const currentFactoryName = ref(""); const getCurrentFactoryName = async () => { @@ -3066,171 +2685,5 @@ display: flex; justify-content: space-between; margin-bottom: 10px; } .print-preview-dialog { .el-dialog__body { padding: 0; max-height: 80vh; overflow-y: auto; } } .print-preview-container { .print-preview-header { padding: 15px; border-bottom: 1px solid #e4e7ed; text-align: center; .el-button { margin: 0 10px; } } .print-preview-content { padding: 20px; background-color: #f5f5f5; min-height: 400px; } } .print-page { width: 220mm; height: 90mm; padding: 10mm; margin: 0 auto; background: white; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); margin-bottom: 10px; box-sizing: border-box; } .delivery-note { width: 100%; height: 100%; font-family: "SimSun", serif; font-size: 10px; line-height: 1.2; display: flex; flex-direction: column; } .header { text-align: center; margin-bottom: 8px; .company-name { font-size: 18px; font-weight: bold; margin-bottom: 4px; } .document-title { font-size: 16px; font-weight: bold; } } .info-section { margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; .info-row { line-height: 20px; .label { font-weight: bold; width: 60px; font-size: 14px; } .value { margin-right: 20px; min-width: 80px; font-size: 14px; } } } .table-section { margin-bottom: 4px; flex: 1; .product-table { width: 100%; border-collapse: collapse; border: 1px solid #000; th, td { border: 1px solid #000; padding: 6px; text-align: center; font-size: 14px; line-height: 1.4; } th { font-weight: bold; } .total-label { text-align: right; font-weight: bold; } .total-value { font-weight: bold; } } } .footer-section { .footer-row { display: flex; margin-bottom: 3px; line-height: 20px; justify-content: space-between; .footer-item { display: flex; margin-right: 20px; .label { font-weight: bold; width: 80px; font-size: 14px; } .value { min-width: 80px; font-size: 14px; } &.address-item { .address-value { min-width: 200px; } } } } } @media print { .app-container { display: none; } .print-page { box-shadow: none; margin: 0; padding: 10mm; padding-left: 20mm; page-break-inside: avoid; page-break-after: always; } .print-page:last-child { page-break-after: avoid; } } </style>