From 5952d34811ee82e797ef0070f84ff041381072a5 Mon Sep 17 00:00:00 2001
From: huminmin <mac@MacBook-Pro.local>
Date: 星期二, 10 三月 2026 17:52:40 +0800
Subject: [PATCH] 新增采购退货单增加费用等数据
---
src/views/projectManagement/Management/components/formDia.vue | 1503 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 1,503 insertions(+), 0 deletions(-)
diff --git a/src/views/projectManagement/Management/components/formDia.vue b/src/views/projectManagement/Management/components/formDia.vue
new file mode 100644
index 0000000..eca4f33
--- /dev/null
+++ b/src/views/projectManagement/Management/components/formDia.vue
@@ -0,0 +1,1503 @@
+<template>
+ <el-dialog
+ v-model="dialogVisible"
+ :title="dialogTitle"
+ width="95%"
+ top="5vh"
+ destroy-on-close
+ @close="closeDialog"
+ >
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-position="top"
+ label-width="120px"
+ :disabled="isView"
+ >
+ <div class="section">
+ <div class="section-header" @click="toggleSection('base')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>鍩虹璧勬枡</span>
+ </div>
+ <el-icon class="toggle-icon">
+ <ArrowDown v-if="sectionCollapsed.base" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ <div v-show="!sectionCollapsed.base" class="section-body">
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="鍗曟嵁缂栧彿" prop="billNo">
+ <el-input v-model="form.billNo" placeholder="绯荤粺鐢熸垚" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="椤圭洰鍚嶇О" prop="projectName">
+ <el-input v-model="form.projectName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customerName">
+ <el-input v-model="form.customerName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="绔嬮」鏃ユ湡" prop="setupDate">
+ <el-date-picker
+ v-model="form.setupDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="椤圭洰鏉ユ簮" prop="projectSource">
+ <el-input v-model="form.projectSource" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="绔嬮」浜�" prop="creatorName">
+ <el-input v-model="form.creatorName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="棰勮宸ユ湡(澶�)" prop="estimatedDays">
+ <el-input-number v-model="form.estimatedDays" :min="0" controls-position="right" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="璁″垝寮�濮嬫棩鏈�" prop="planStartDate">
+ <el-date-picker
+ v-model="form.planStartDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="璁″垝瀹屾垚鏃ユ湡" prop="planEndDate">
+ <el-date-picker
+ v-model="form.planEndDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="椤圭洰绫诲瀷" prop="projectManagementPlanId">
+ <el-select v-model="form.projectManagementPlanId" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="opt in projectTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="椤圭洰閲戦" prop="projectAmount">
+ <el-input-number v-model="form.projectAmount" :min="0" controls-position="right" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="瀹℃牳鐘舵��" prop="auditStatus">
+ <el-select v-model="form.auditStatus" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="d in project_management" :key="d.value" :label="d.label" :value="d.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+
+ </el-row>
+ <el-row :gutter="10" >
+ <el-col :span="24">
+ <el-upload
+ v-model:file-list="fileList"
+ :action="upload.url"
+ :headers="upload.headers"
+ multiple
+ :disabled="isView"
+ :before-upload="beforeUpload"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ name="files"
+ :on-remove="handleRemove"
+ >
+ <el-button type="primary" :disabled="isView">涓婁紶鏂囦欢</el-button>
+ </el-upload>
+ <div v-if="existingAttachments.length > 0" class="attachment-list">
+ <div
+ v-for="(att, idx) in existingAttachments"
+ :key="att.id || att.url || idx"
+ class="attachment-item"
+ >
+ <el-icon><Document /></el-icon>
+ <span class="attachment-name">{{ att.name || att.fileName || att.url || '闄勪欢' }}</span>
+ <el-button link type="primary" size="small" @click="downloadAttachment(att)">涓嬭浇</el-button>
+ </div>
+ </div>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏�" maxlength="100" show-word-limit />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+ </div>
+
+ <div class="section">
+ <div class="section-header" @click="toggleSection('product')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>浜у搧淇℃伅</span>
+ </div>
+ <div class="section-actions" @click.stop>
+ <el-button v-if="!isView" type="primary" @click="openProductForm('add')">娣诲姞</el-button>
+ <el-button v-if="!isView" plain type="danger" @click="deleteProduct">鍒犻櫎</el-button>
+ <el-icon class="toggle-icon" @click="toggleSection('product')">
+ <ArrowDown v-if="sectionCollapsed.product" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ </div>
+ <div v-show="!sectionCollapsed.product" class="section-body">
+ <el-table
+ :data="productData"
+ border
+ show-summary
+ :summary-method="summarizeProductTable"
+ @selection-change="productSelected"
+ >
+ <el-table-column v-if="!isView" align="center" type="selection" width="55" />
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="浜у搧澶х被" prop="productCategory" />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" />
+ <el-table-column label="鍗曚綅" prop="unit" />
+ <el-table-column label="鏁伴噺" prop="quantity" />
+ <el-table-column label="绋庣巼(%)" prop="taxRate" />
+ <el-table-column label="鍚◣鍗曚环(鍏�)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" />
+ <el-table-column label="鍚◣鎬讳环(鍏�)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" />
+ <el-table-column label="涓嶅惈绋庢�讳环(鍏�)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" />
+ <el-table-column v-if="!isView" fixed="right" label="鎿嶄綔" min-width="60" align="center">
+ <template #default="scope">
+ <el-button link type="primary" size="small" @click="openProductForm('edit', scope.row, scope.$index)">缂栬緫</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+
+ <div class="section">
+ <div class="section-header" @click="toggleSection('team')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>椤圭洰鍥㈤槦</span>
+ </div>
+ <div class="section-actions" @click.stop>
+ <el-button v-if="!isView" type="primary" :icon="Plus" @click="addTeamRow">鏂板琛�</el-button>
+ <el-icon class="toggle-icon" @click="toggleSection('team')">
+ <ArrowDown v-if="sectionCollapsed.team" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ </div>
+ <div v-show="!sectionCollapsed.team" class="section-body">
+ <PIMTable
+ :column="teamColumns"
+ :tableData="form.teamList"
+ :tableLoading="false"
+ :isSelection="false"
+ :isShowPagination="false"
+ height="220"
+ >
+ <template #memberId="{ row }">
+ <el-select v-model="row.memberId" placeholder="璇烽�夋嫨" filterable clearable style="width: 100%" :disabled="isView">
+ <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
+ </el-select>
+ </template>
+ <template #roleId="{ row }">
+ <el-select v-model="row.roleId" placeholder="璇烽�夋嫨" clearable style="width: 100%" :disabled="isView">
+ <el-option v-for="r in roleOptions" :key="r.value" :label="r.label" :value="r.value" />
+ </el-select>
+ </template>
+ <template #enterDate="{ row }">
+ <el-date-picker
+ v-model="row.enterDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #leaveDate="{ row }">
+ <el-date-picker
+ v-model="row.leaveDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #phone="{ row }">
+ <el-input v-model="row.phone" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #teamRemark="{ row }">
+ <el-input v-model="row.remark" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #teamAction="{ row, index }">
+ <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removeTeamRow(index)">鍒犻櫎</el-button>
+ <span v-else>鈥�</span>
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+
+ <!-- <div class="section">
+ <div class="section-header" @click="toggleSection('phase')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>椤圭洰闃舵</span>
+ </div>
+ <div class="section-actions" @click.stop>
+ <el-button v-if="!isView" type="primary" :icon="Plus" @click="addPhaseRow">鏂板琛�</el-button>
+ <el-icon class="toggle-icon" @click="toggleSection('phase')">
+ <ArrowDown v-if="sectionCollapsed.phase" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ </div>
+ <div v-show="!sectionCollapsed.phase" class="section-body">
+ <PIMTable
+ :column="phaseColumns"
+ :tableData="form.phaseList"
+ :tableLoading="false"
+ :isSelection="false"
+ :isShowPagination="false"
+ height="240"
+ >
+ <template #phaseName="{ row }">
+ <el-input v-model="row.phaseName" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #phaseDesc="{ row }">
+ <el-input v-model="row.description" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #ownerId="{ row }">
+ <el-select v-model="row.ownerId" placeholder="璇烽�夋嫨" filterable clearable style="width: 100%" :disabled="isView">
+ <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
+ </el-select>
+ </template>
+ <template #planDays="{ row }">
+ <el-input-number v-model="row.planDays" :min="0" controls-position="right" style="width: 100%" :disabled="isView" />
+ </template>
+ <template #planStart="{ row }">
+ <el-date-picker
+ v-model="row.planStartDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #planEnd="{ row }">
+ <el-date-picker
+ v-model="row.planEndDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #progress="{ row }">
+ <el-input-number v-model="row.progress" :min="0" :max="100" controls-position="right" style="width: 100%" :disabled="isView" />
+ </template>
+ <template #actualStart="{ row }">
+ <el-date-picker
+ v-model="row.actualStartDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #actualEnd="{ row }">
+ <el-date-picker
+ v-model="row.actualEndDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #overdueDays="{ row }">
+ <el-input-number v-model="row.overdueDays" :min="0" controls-position="right" style="width: 100%" :disabled="isView" />
+ </template>
+ <template #completion="{ row }">
+ <el-input v-model="row.completionRemark" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #phaseAction="{ row, index }">
+ <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removePhaseRow(index)">鍒犻櫎</el-button>
+ <span v-else>鈥�</span>
+ </template>
+ </PIMTable>
+ </div>
+ </div> -->
+
+ <div class="section">
+ <div class="section-header" @click="toggleSection('address')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>鏀惰揣鍦板潃</span>
+ </div>
+ <div class="section-actions" @click.stop>
+ <el-button v-if="!isView" type="primary" :icon="Plus" @click="addAddressRow">鏂板琛�</el-button>
+ <el-icon class="toggle-icon" @click="toggleSection('address')">
+ <ArrowDown v-if="sectionCollapsed.address" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ </div>
+ <div v-show="!sectionCollapsed.address" class="section-body">
+ <PIMTable
+ :column="addressColumns"
+ :tableData="form.addressList"
+ :tableLoading="false"
+ :isSelection="false"
+ :isShowPagination="false"
+ height="200"
+ >
+ <template #receiver="{ row }">
+ <el-input v-model="row.receiver" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #receiverPhone="{ row }">
+ <el-input v-model="row.phone" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #receiverAddress="{ row }">
+ <el-input v-model="row.address" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #addressAction="{ row, index }">
+ <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removeAddressRow(index)">鍒犻櫎</el-button>
+ <span v-else>鈥�</span>
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+
+ <div class="section">
+ <div class="section-header" @click="toggleSection('contact')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>鑱旂郴淇℃伅</span>
+ </div>
+ <el-icon class="toggle-icon">
+ <ArrowDown v-if="sectionCollapsed.contact" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ <div v-show="!sectionCollapsed.contact" class="section-body">
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="鑱旂郴浜哄鍚�" prop="contactName">
+ <el-input v-model="form.contactName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鎬у埆" prop="contactGender">
+ <el-select v-model="form.contactGender" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option label="鐢�" value="1" />
+ <el-option label="濂�" value="2" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鐢熸棩" prop="contactBirthday">
+ <el-date-picker
+ v-model="form.contactBirthday"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="閭" prop="contactEmail">
+ <el-input v-model="form.contactEmail" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="閮ㄩ棬" prop="contactDept">
+ <el-input v-model="form.contactDept" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鑱屽姟" prop="contactJob">
+ <el-input v-model="form.contactJob" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鎵嬫満鍙风爜" prop="contactMobile">
+ <el-input v-model="form.contactMobile" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="寰俊鍙风爜" prop="contactWechat">
+ <el-input v-model="form.contactWechat" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="QQ" prop="contactQq">
+ <el-input v-model="form.contactQq" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="浼佷笟寰俊" prop="contactWorkWechat">
+ <el-input v-model="form.contactWorkWechat" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍦板潃" prop="contactAddress">
+ <el-input v-model="form.contactAddress" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="contactRemark">
+ <el-input v-model="form.contactRemark" type="textarea" :rows="2" placeholder="璇疯緭鍏�" maxlength="200" show-word-limit />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+ </div>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button v-if="!isView" type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDialog">{{ isView ? '鍏抽棴' : '鍙栨秷' }}</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <FormDialog
+ v-model="productFormVisible"
+ :title="productOperationType === 'add' ? '鏂板浜у搧' : '缂栬緫浜у搧'"
+ :width="'40%'"
+ :operation-type="productOperationType"
+ @close="closeProductDia"
+ @confirm="submitProduct"
+ @cancel="closeProductDia"
+ >
+ <el-form ref="productFormRef" :model="productForm" label-width="140px" label-position="top" :rules="productRules">
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="浜у搧澶х被锛�" prop="productCategoryId">
+ <el-tree-select
+ v-model="productForm.productCategoryId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ check-strictly
+ :data="productCategoryOptions"
+ :render-after-expand="false"
+ style="width: 100%"
+ @change="getModels"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="瑙勬牸鍨嬪彿锛�" prop="productModelId">
+ <el-select v-model="productForm.productModelId" placeholder="璇烽�夋嫨" clearable filterable @change="getProductModel">
+ <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍗曚綅锛�" prop="unit">
+ <el-input v-model="productForm.unit" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绋庣巼(%)锛�" prop="taxRate">
+ <el-select v-model="productForm.taxRate" placeholder="璇烽�夋嫨" clearable @change="calculateFromTaxRate">
+ <el-option label="1" value="1" />
+ <el-option label="6" value="6" />
+ <el-option label="13" value="13" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍚◣鍗曚环(鍏�)锛�" prop="taxInclusiveUnitPrice">
+ <el-input-number
+ v-model="productForm.taxInclusiveUnitPrice"
+ :step="0.01"
+ :min="0"
+ :precision="2"
+ style="width: 100%"
+ placeholder="璇疯緭鍏�"
+ clearable
+ @change="calculateFromUnitPrice"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏁伴噺锛�" prop="quantity">
+ <el-input-number
+ v-model="productForm.quantity"
+ :step="0.1"
+ :min="0"
+ :precision="2"
+ style="width: 100%"
+ placeholder="璇疯緭鍏�"
+ clearable
+ @change="calculateFromQuantity"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍚◣鎬讳环(鍏�)锛�" prop="taxInclusiveTotalPrice">
+ <el-input v-model="productForm.taxInclusiveTotalPrice" placeholder="璇疯緭鍏�" clearable @change="calculateFromTotalPrice" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓嶅惈绋庢�讳环(鍏�)锛�" prop="taxExclusiveTotalPrice">
+ <el-input v-model="productForm.taxExclusiveTotalPrice" placeholder="璇疯緭鍏�" clearable @change="calculateFromExclusiveTotalPrice" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍙戠エ绫诲瀷锛�" prop="invoiceType">
+ <el-select v-model="productForm.invoiceType" placeholder="璇烽�夋嫨" clearable>
+ <el-option label="澧炴櫘绁�" value="澧炴櫘绁�" />
+ <el-option label="澧炰笓绁�" value="澧炰笓绁�" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </FormDialog>
+</template>
+
+<script setup name="ProjectManagementFormDia">
+import { computed, getCurrentInstance, reactive, ref, toRefs } from 'vue'
+import { ArrowDown, ArrowUp, Delete, Plus, Document } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { getToken } from '@/utils/auth'
+import PIMTable from '@/components/PIMTable/PIMTable.vue'
+import FormDialog from '@/components/Dialog/FormDialog.vue'
+import { listPlan } from '@/api/projectManagement/projectType'
+import { findRoleListPage } from '@/api/projectManagement/role'
+import { userListAll } from '@/api/publicApi'
+import { addProject, getProject, updateProject } from '@/api/projectManagement/project'
+import { modelList, productTreeList } from '@/api/basicData/product'
+import { delProduct as delSalesProduct } from '@/api/salesManagement/salesLedger'
+
+const emit = defineEmits(['completed'])
+const { proxy } = getCurrentInstance()
+const { bill_status, project_management, plan_status } = proxy.useDict('bill_status', 'project_management', 'plan_status')
+
+const dialogVisible = ref(false)
+const operationType = ref('add')
+const formRef = ref()
+const fileList = ref([])
+const existingAttachments = ref([])
+const upload = reactive({
+ url: import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload',
+ headers: { Authorization: 'Bearer ' + getToken() }
+})
+
+const projectTypeOptions = ref([])
+const roleOptions = ref([])
+const userOptions = ref([])
+const productData = ref([])
+const productSelectedRows = ref([])
+const productCategoryOptions = ref([])
+const modelOptions = ref([])
+const productFormVisible = ref(false)
+const productOperationType = ref('add')
+const productFormRef = ref()
+const productIndex = ref(0)
+const isCalculating = ref(false)
+
+const productFormData = reactive({
+ productForm: {
+ productCategoryId: undefined,
+ productCategory: '',
+ productModelId: undefined,
+ specificationModel: '',
+ unit: '',
+ quantity: '',
+ taxInclusiveUnitPrice: '',
+ taxRate: '',
+ taxInclusiveTotalPrice: '',
+ taxExclusiveTotalPrice: '',
+ invoiceType: ''
+ },
+ productRules: {
+ productCategoryId: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }],
+ productModelId: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }],
+ unit: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ quantity: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ taxInclusiveUnitPrice: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ taxRate: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }],
+ taxInclusiveTotalPrice: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ taxExclusiveTotalPrice: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ invoiceType: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }]
+ }
+})
+
+const { productForm, productRules } = toRefs(productFormData)
+
+const data = reactive({
+ form: {
+ id: undefined,
+ clientId: undefined,
+ parentProjectId: undefined,
+ projectManagementPlanId: undefined,
+ managerId: undefined,
+ salesmanId: undefined,
+ salesmanName: '',
+ actualStartDate: '',
+ actualEndDate: '',
+ departmentId: undefined,
+ departmentName: '',
+ orderDate: '',
+ billNo: '',
+ projectName: '',
+ customerName: '',
+ parentProjectName: '',
+ setupDate: '',
+ projectSource: '',
+ creatorName: '',
+ billStatus: '',
+ projectStage: '',
+ estimatedDays: 0,
+ planStartDate: '',
+ planEndDate: '',
+ projectManagementPlanId: undefined,
+ projectAmount: 0,
+ auditStatus: '',
+ remark: '',
+ attachmentIds: [],
+ teamList: [],
+ phaseList: [],
+ addressList: [],
+ contactName: '',
+ contactGender: '',
+ contactBirthday: '',
+ contactEmail: '',
+ contactDept: '',
+ contactJob: '',
+ contactMobile: '',
+ contactWechat: '',
+ contactQq: '',
+ contactWorkWechat: '',
+ contactAddress: '',
+ contactRemark: ''
+ },
+ rules: {
+ projectName: [{ required: true, message: '璇疯緭鍏ラ」鐩悕绉�', trigger: 'blur' }]
+ }
+})
+
+const { form, rules } = toRefs(data)
+
+const sectionCollapsed = reactive({
+ base: false,
+ product: false,
+ team: false,
+ phase: false,
+ address: false,
+ contact: false
+})
+
+const isView = computed(() => operationType.value === 'view')
+const dialogTitle = computed(() => {
+ if (operationType.value === 'add') return '鏂板椤圭洰'
+ if (operationType.value === 'edit') return '缂栬緫椤圭洰'
+ return '椤圭洰璇︽儏'
+})
+
+const teamColumns = [
+ { label: '濮撳悕', prop: 'memberId', align: 'center', width: 180, dataType: 'slot', slot: 'memberId' },
+ { label: '椤圭洰缁勮鑹�', prop: 'roleId', align: 'center', width: 160, dataType: 'slot', slot: 'roleId' },
+ { label: '杩涘叆鏃ユ湡', prop: 'enterDate', align: 'center', width: 160, dataType: 'slot', slot: 'enterDate' },
+ { label: '绂诲紑鏃ユ湡', prop: 'leaveDate', align: 'center', width: 160, dataType: 'slot', slot: 'leaveDate' },
+ { label: '鑱旂郴鏂瑰紡', prop: 'phone', align: 'center', width: 180, dataType: 'slot', slot: 'phone' },
+ { label: '澶囨敞', prop: 'remark', align: 'center', dataType: 'slot', slot: 'teamRemark' },
+ { label: '鎿嶄綔', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'teamAction', fixed: 'right' }
+]
+
+const phaseColumns = [
+ { label: '闃舵鍚嶇О', prop: 'phaseName', align: 'center', width: 160, dataType: 'slot', slot: 'phaseName' },
+ { label: '鎻忚堪', prop: 'description', align: 'center', width: 200, dataType: 'slot', slot: 'phaseDesc' },
+ { label: '璐熻矗浜�', prop: 'ownerId', align: 'center', width: 160, dataType: 'slot', slot: 'ownerId' },
+ { label: '棰勮宸ユ湡(澶�)', prop: 'planDays', align: 'center', width: 140, dataType: 'slot', slot: 'planDays' },
+ { label: '璁″垝寮�濮嬫棩鏈�', prop: 'planStartDate', align: 'center', width: 160, dataType: 'slot', slot: 'planStart' },
+ { label: '璁″垝缁撴潫鏃ユ湡', prop: 'planEndDate', align: 'center', width: 160, dataType: 'slot', slot: 'planEnd' },
+ { label: '杩涘害(%)', prop: 'progress', align: 'center', width: 120, dataType: 'slot', slot: 'progress' },
+ { label: '瀹為檯寮�濮嬫棩鏈�', prop: 'actualStartDate', align: 'center', width: 160, dataType: 'slot', slot: 'actualStart' },
+ { label: '瀹為檯缁撴潫鏃ユ湡', prop: 'actualEndDate', align: 'center', width: 160, dataType: 'slot', slot: 'actualEnd' },
+ { label: '閫炬湡澶╂暟', prop: 'overdueDays', align: 'center', width: 120, dataType: 'slot', slot: 'overdueDays' },
+ { label: '瀹屾垚鎯呭喌', prop: 'completionRemark', align: 'center', width: 200, dataType: 'slot', slot: 'completion' },
+ { label: '鎿嶄綔', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'phaseAction', fixed: 'right' }
+]
+
+const addressColumns = [
+ { label: '鏀惰揣浜�', prop: 'receiver', align: 'center', width: 180, dataType: 'slot', slot: 'receiver' },
+ { label: '鑱旂郴鏂瑰紡', prop: 'phone', align: 'center', width: 180, dataType: 'slot', slot: 'receiverPhone' },
+ { label: '鏀惰揣鍦板潃', prop: 'address', align: 'center', dataType: 'slot', slot: 'receiverAddress' },
+ { label: '鎿嶄綔', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'addressAction', fixed: 'right' }
+]
+
+function toggleSection(key) {
+ sectionCollapsed[key] = !sectionCollapsed[key]
+}
+
+function resetFormData() {
+ Object.assign(form.value, {
+ id: undefined,
+ clientId: undefined,
+ parentProjectId: undefined,
+ projectManagementPlanId: undefined,
+ managerId: undefined,
+ salesmanId: undefined,
+ salesmanName: '',
+ actualStartDate: '',
+ actualEndDate: '',
+ departmentId: undefined,
+ departmentName: '',
+ orderDate: '',
+ billNo: '',
+ projectName: '',
+ customerName: '',
+ parentProjectName: '',
+ setupDate: '',
+ projectSource: '',
+ creatorName: '',
+ billStatus: '',
+ projectStage: '',
+ estimatedDays: 0,
+ planStartDate: '',
+ planEndDate: '',
+ projectManagementPlanId: undefined,
+ projectAmount: 0,
+ auditStatus: '',
+ remark: '',
+ attachmentIds: [],
+ teamList: [],
+ phaseList: [],
+ addressList: [],
+ contactName: '',
+ contactGender: '',
+ contactBirthday: '',
+ contactEmail: '',
+ contactDept: '',
+ contactJob: '',
+ contactMobile: '',
+ contactWechat: '',
+ contactQq: '',
+ contactWorkWechat: '',
+ contactAddress: '',
+ contactRemark: ''
+ })
+ fileList.value = []
+ productData.value = []
+}
+
+function formattedNumber(row, column, cellValue) {
+ const val = Number(cellValue ?? 0)
+ return Number.isFinite(val) ? val.toFixed(2) : '0.00'
+}
+
+function summarizeProductTable(param) {
+ return proxy.summarizeTable(param, ['taxInclusiveTotalPrice', 'taxExclusiveTotalPrice'])
+}
+
+function productSelected(selection) {
+ productSelectedRows.value = selection
+}
+
+function convertIdToValue(data) {
+ return (Array.isArray(data) ? data : []).map(item => {
+ const { id, children, ...rest } = item
+ const newItem = {
+ ...rest,
+ value: id
+ }
+ if (children && children.length > 0) {
+ newItem.children = convertIdToValue(children)
+ }
+ return newItem
+ })
+}
+
+function findNodeById(nodes, productId) {
+ for (let i = 0; i < (nodes || []).length; i++) {
+ if (nodes[i].value === productId) {
+ return nodes[i].label
+ }
+ if (nodes[i].children && nodes[i].children.length > 0) {
+ const foundNode = findNodeById(nodes[i].children, productId)
+ if (foundNode) return foundNode
+ }
+ }
+ return null
+}
+
+function findNodeIdByLabel(nodes, label) {
+ if (!label) return null
+ for (let i = 0; i < (nodes || []).length; i++) {
+ const node = nodes[i]
+ if (node.label === label) return node.value
+ if (node.children && node.children.length > 0) {
+ const found = findNodeIdByLabel(node.children, label)
+ if (found !== null && found !== undefined) return found
+ }
+ }
+ return null
+}
+
+function getProductOptions() {
+ return productTreeList().then(res => {
+ const list = res?.data || res
+ productCategoryOptions.value = convertIdToValue(list)
+ return productCategoryOptions.value
+ })
+}
+
+function getModels(value) {
+ const categoryLabel = findNodeById(productCategoryOptions.value, value)
+ productForm.value.productCategory = categoryLabel || ''
+ modelList({ id: value }).then(res => {
+ modelOptions.value = res?.data || res || []
+ })
+}
+
+function getProductModel(value) {
+ 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 = ''
+ productForm.value.unit = ''
+ }
+}
+
+async function openProductForm(type, row, index) {
+ productOperationType.value = type
+ productIndex.value = index || 0
+ productForm.value = {}
+ proxy.resetForm('productFormRef')
+
+ if (!productCategoryOptions.value || productCategoryOptions.value.length === 0) {
+ await getProductOptions()
+ }
+
+ if (type === 'edit' && row) {
+ productForm.value = { ...row }
+ try {
+ const categoryId = findNodeIdByLabel(productCategoryOptions.value, productForm.value.productCategory)
+ if (categoryId) {
+ productForm.value.productCategoryId = categoryId
+ const models = await modelList({ id: categoryId })
+ modelOptions.value = models?.data || models || []
+ const currentModel = (modelOptions.value || []).find(m => m.model === productForm.value.specificationModel)
+ if (currentModel) {
+ productForm.value.productModelId = currentModel.id
+ }
+ }
+ } catch {}
+ } else {
+ productForm.value = {
+ productCategoryId: undefined,
+ productCategory: '',
+ productModelId: undefined,
+ specificationModel: '',
+ unit: '',
+ quantity: '',
+ taxInclusiveUnitPrice: '',
+ taxRate: '',
+ taxInclusiveTotalPrice: '',
+ taxExclusiveTotalPrice: '',
+ invoiceType: ''
+ }
+ }
+
+ productFormVisible.value = true
+}
+
+function closeProductDia() {
+ proxy.resetForm('productFormRef')
+ productFormVisible.value = false
+}
+
+function submitProduct() {
+ productFormRef.value?.validate?.(valid => {
+ if (!valid) return
+ const payload = { ...productForm.value }
+ if (productOperationType.value === 'add') {
+ productData.value.push(payload)
+ } else {
+ productData.value[productIndex.value] = payload
+ }
+ closeProductDia()
+ })
+}
+
+function deleteProduct() {
+ if (!productSelectedRows.value || productSelectedRows.value.length === 0) {
+ proxy.$modal?.msgWarning?.('璇烽�夋嫨鏁版嵁')
+ return
+ }
+ const selectedIds = productSelectedRows.value.map(r => r?.id).filter(Boolean)
+ if (operationType.value !== 'add' && selectedIds.length > 0) {
+ delSalesProduct(selectedIds)
+ .then(() => {
+ proxy.$modal?.msgSuccess?.('鍒犻櫎鎴愬姛')
+ productData.value = productData.value.filter(row => !selectedIds.includes(row?.id))
+ productSelectedRows.value = []
+ })
+ .catch(() => {})
+ return
+ }
+
+ productData.value = productData.value.filter(row => !productSelectedRows.value.includes(row))
+ productSelectedRows.value = []
+}
+
+function calculateFromTotalPrice() {
+ if (isCalculating.value) return
+ const totalPrice = parseFloat(productForm.value.taxInclusiveTotalPrice)
+ const quantity = parseFloat(productForm.value.quantity)
+ if (!totalPrice || !quantity || quantity <= 0) return
+ isCalculating.value = true
+ productForm.value.taxInclusiveUnitPrice = (totalPrice / quantity).toFixed(2)
+ if (productForm.value.taxRate) {
+ productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(totalPrice, productForm.value.taxRate)
+ }
+ isCalculating.value = false
+}
+
+function calculateFromExclusiveTotalPrice() {
+ if (!productForm.value.taxRate) {
+ proxy.$modal?.msgWarning?.('璇峰厛閫夋嫨绋庣巼')
+ return
+ }
+ if (isCalculating.value) return
+ const exclusiveTotalPrice = parseFloat(productForm.value.taxExclusiveTotalPrice)
+ const quantity = parseFloat(productForm.value.quantity)
+ const taxRate = parseFloat(productForm.value.taxRate)
+ if (!exclusiveTotalPrice || !quantity || quantity <= 0 || !taxRate) return
+ isCalculating.value = true
+ const taxRateDecimal = taxRate / 100
+ const inclusiveTotalPrice = exclusiveTotalPrice / (1 - taxRateDecimal)
+ productForm.value.taxInclusiveTotalPrice = inclusiveTotalPrice.toFixed(2)
+ productForm.value.taxInclusiveUnitPrice = (inclusiveTotalPrice / quantity).toFixed(2)
+ isCalculating.value = false
+}
+
+function calculateFromQuantity() {
+ if (!productForm.value.taxRate) {
+ proxy.$modal?.msgWarning?.('璇峰厛閫夋嫨绋庣巼')
+ return
+ }
+ if (isCalculating.value) return
+ const quantity = parseFloat(productForm.value.quantity)
+ const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice)
+ if (!quantity || quantity <= 0 || !unitPrice) return
+ isCalculating.value = true
+ productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2)
+ if (productForm.value.taxRate) {
+ productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(
+ productForm.value.taxInclusiveTotalPrice,
+ productForm.value.taxRate
+ )
+ }
+ isCalculating.value = false
+}
+
+function calculateFromUnitPrice() {
+ if (!productForm.value.taxRate) {
+ proxy.$modal?.msgWarning?.('璇峰厛閫夋嫨绋庣巼')
+ return
+ }
+ if (isCalculating.value) return
+ const quantity = parseFloat(productForm.value.quantity)
+ const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice)
+ if (!quantity || quantity <= 0 || !unitPrice) return
+ isCalculating.value = true
+ productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2)
+ if (productForm.value.taxRate) {
+ productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(
+ productForm.value.taxInclusiveTotalPrice,
+ productForm.value.taxRate
+ )
+ }
+ isCalculating.value = false
+}
+
+function calculateFromTaxRate() {
+ if (!productForm.value.taxRate) {
+ proxy.$modal?.msgWarning?.('璇峰厛閫夋嫨绋庣巼')
+ return
+ }
+ if (isCalculating.value) return
+ const inclusiveTotalPrice = parseFloat(productForm.value.taxInclusiveTotalPrice)
+ const taxRate = parseFloat(productForm.value.taxRate)
+ if (!inclusiveTotalPrice || !taxRate) return
+ isCalculating.value = true
+ productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(inclusiveTotalPrice, taxRate)
+ isCalculating.value = false
+}
+
+async function loadProjectTypeOptions() {
+ try {
+ const res = await listPlan({ current: 1, size: 999 })
+ const records = res?.data?.records || res?.records || res?.rows || []
+ projectTypeOptions.value = records.map(item => ({ label: item.name, value: item.id }))
+ } catch {
+ projectTypeOptions.value = []
+ }
+}
+
+async function loadRoleOptions() {
+ try {
+ const res = await findRoleListPage({ pageNum: 1, pageSize: 999 })
+ const records = res?.data?.records || res?.rows || res?.records || []
+ roleOptions.value = records.map(item => ({ label: item.roleName || item.name, value: item.id }))
+ } catch {
+ roleOptions.value = []
+ }
+}
+
+async function loadUserOptions() {
+ try {
+ const res = await userListAll()
+ const list = res?.data || res?.rows || res || []
+ userOptions.value = (Array.isArray(list) ? list : []).map(u => ({
+ label: u.nickName || u.userName || u.username || u.name,
+ value: u.userId || u.id
+ }))
+ } catch {
+ userOptions.value = []
+ }
+}
+
+function addTeamRow() {
+ form.value.teamList.push({
+ memberId: undefined,
+ roleId: undefined,
+ enterDate: '',
+ leaveDate: '',
+ phone: '',
+ remark: ''
+ })
+}
+
+function removeTeamRow(index) {
+ if (index > -1) form.value.teamList.splice(index, 1)
+}
+
+function addPhaseRow() {
+ form.value.phaseList.push({
+ phaseName: '',
+ description: '',
+ ownerId: undefined,
+ planDays: 0,
+ planStartDate: '',
+ planEndDate: '',
+ progress: 0,
+ actualStartDate: '',
+ actualEndDate: '',
+ overdueDays: 0,
+ completionRemark: ''
+ })
+}
+
+function removePhaseRow(index) {
+ if (index > -1) form.value.phaseList.splice(index, 1)
+}
+
+function addAddressRow() {
+ form.value.addressList.push({
+ receiver: '',
+ phone: '',
+ address: ''
+ })
+}
+
+function removeAddressRow(index) {
+ if (index > -1) form.value.addressList.splice(index, 1)
+}
+
+function beforeUpload() {
+ if (isView.value) return false
+ proxy.$modal?.loading?.('姝e湪涓婁紶鏂囦欢锛岃绋嶅��...')
+ return true
+}
+
+function handleUploadError() {
+ proxy.$modal?.closeLoading?.()
+ ElMessage.error('涓婁紶鏂囦欢澶辫触')
+}
+
+function handleUploadSuccess(res, file) {
+ console.log(res, file)
+ proxy.$modal?.closeLoading?.()
+ if (res?.code !== 200) {
+ ElMessage.error(res?.msg || '涓婁紶澶辫触')
+ return
+ }
+ const attachmentId = res?.data?.[0]?.id ?? ""
+ if (!attachmentId) return
+ form.value.attachmentIds.push(attachmentId)
+ console.log(form.value.attachmentIds)
+ ElMessage.success('涓婁紶鎴愬姛')
+}
+
+function handleRemove(file) {
+ const attachmentId = file?.attachmentId
+ if (!attachmentId) return
+ form.value.attachmentIds = (form.value.attachmentIds || []).filter(id => id !== attachmentId)
+}
+
+async function openDialog(payload = {}) {
+ operationType.value = payload.operationType || 'add'
+ resetFormData()
+ await Promise.all([loadProjectTypeOptions(), loadRoleOptions(), loadUserOptions(), getProductOptions()])
+ if (payload.row?.id) {
+ try {
+ const res = await getProject(payload.row.id)
+ const detail = res?.data?.data ?? res?.data ?? res
+ const info = detail?.info || {}
+ const shippingAddress = detail?.shippingAddress || {}
+ const contractInfo = detail?.contractInfo || {}
+
+ const normalizeId = v => {
+ if (v === undefined || v === null || v === '') return undefined
+ const n = Number(v)
+ return Number.isNaN(n) ? v : n
+ }
+
+ const normalizeDictValue = v => {
+ if (v === undefined || v === null || v === '') return ''
+ return String(v)
+ }
+
+ const computeEstimatedDays = (start, end) => {
+ if (!start || !end) return 0
+ const startTime = new Date(`${start}T00:00:00`).getTime()
+ const endTime = new Date(`${end}T00:00:00`).getTime()
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) return 0
+ if (endTime < startTime) return 0
+ return Math.floor((endTime - startTime) / (24 * 60 * 60 * 1000)) + 1
+ }
+
+ Object.assign(form.value, {
+ id: info.id,
+ billNo: info.no ?? '',
+ projectManagementPlanId: info.projectManagementPlanId ?? '',
+ estimatedDays: Number(info.estimatedDays) || computeEstimatedDays(info.planStartTime, info.planEndTime) || 0,
+ projectName: info.title ?? '',
+ customerName: info.clientName ?? '',
+ parentProjectName: info.projectManagementInfoParentName ?? '',
+ setupDate: info.establishTime ?? '',
+ projectSource: info.source ?? '',
+ creatorName: info.managerName ?? '',
+ billStatus: normalizeDictValue(info.status),
+ projectStage: normalizeDictValue(info.stage ?? info.projectStage),
+ planStartDate: info.planStartTime ?? '',
+ planEndDate: info.planEndTime ?? '',
+ projectAmount: info.orderAmount ?? 0,
+ auditStatus: normalizeDictValue(info.reviewStatus),
+ remark: info.remark ?? '',
+ attachmentIds: Array.isArray(info.attachmentIds) ? info.attachmentIds : [],
+ teamList: Array.isArray(info.teamList) ? info.teamList.map(t => ({
+ memberId: normalizeId(t.userId),
+ roleId: normalizeId(t.userRoleId),
+ enterDate: t.joinTime,
+ leaveDate: t.departTime,
+ phone: t.contact,
+ remark: t.remark
+ })) : [],
+ addressList: shippingAddress?.address
+ ? [{
+ receiver: shippingAddress.consignee,
+ phone: shippingAddress.contract,
+ address: shippingAddress.address
+ }]
+ : [],
+ contactName: contractInfo.name ?? '',
+ contactGender: contractInfo.sex === '鐢�' ? '1' : contractInfo.sex === '濂�' ? '2' : '',
+ contactBirthday: contractInfo.birthday ?? '',
+ contactDept: contractInfo.department ?? '',
+ contactJob: contractInfo.job ?? '',
+ contactMobile: contractInfo.phoneNumber ?? '',
+ contactEmail: contractInfo.email ?? '',
+ contactQq: contractInfo.qq ?? '',
+ contactWechat: contractInfo.wx ?? '',
+ contactWorkWechat: contractInfo.lineaFissa ?? '',
+ contactAddress: contractInfo.origineEtnica ?? '',
+ contactRemark: contractInfo.rappresentanteLegale ?? ''
+ })
+
+ existingAttachments.value = Array.isArray(info.attachmentList)
+ ? info.attachmentList.map(a => ({
+ id: a.id ?? a.fileId,
+ name: a.fileName ?? a.name,
+ url: a.url ?? a.fileUrl ?? a.path
+ }))
+ : []
+
+ const rawPhaseList =
+ detail?.phaseList ||
+ detail?.projectPhaseList ||
+ detail?.projectStageList ||
+ info?.phaseList ||
+ info?.projectPhaseList ||
+ []
+ form.value.phaseList = Array.isArray(rawPhaseList)
+ ? rawPhaseList.map(p => ({
+ phaseName: p.phaseName ?? p.name ?? p.title ?? '',
+ description: p.description ?? p.workContent ?? p.desc ?? '',
+ ownerId: normalizeId(p.ownerId ?? p.leaderId ?? p.userId),
+ planDays: Number(p.planDays ?? p.estimatedDuration ?? p.estimatedDays) || 0,
+ planStartDate: p.planStartDate ?? p.planStartTime ?? p.startDate ?? '',
+ planEndDate: p.planEndDate ?? p.planEndTime ?? p.endDate ?? '',
+ progress: Number(p.progress ?? p.schedule) || 0,
+ actualStartDate: p.actualStartDate ?? p.actualStartTime ?? '',
+ actualEndDate: p.actualEndDate ?? p.actualEndTime ?? '',
+ overdueDays: Number(p.overdueDays ?? p.overDays) || 0,
+ completionRemark: p.completionRemark ?? p.remark ?? ''
+ }))
+ : []
+
+ productData.value = detail?.salesLedgerProductList || detail?.productData || []
+ } catch {}
+ }
+ if (form.value.teamList.length === 0 && !isView.value) addTeamRow()
+ if (form.value.phaseList.length === 0 && !isView.value) addPhaseRow()
+ dialogVisible.value = true
+}
+
+function downloadAttachment(att) {
+ if (att?.name) {
+ try {
+ proxy.$download.name(att.url);
+ return
+ } catch (e) {}
+ }
+ ElMessage.warning('闄勪欢鏆傛棤涓嬭浇鍦板潃')
+}
+function closeDialog() {
+ dialogVisible.value = false
+}
+
+async function submitForm() {
+ if (isView.value) {
+ closeDialog()
+ return
+ }
+ await formRef.value?.validate?.()
+ if (!productData.value || productData.value.length === 0) {
+ proxy.$modal?.msgWarning?.('璇锋坊鍔犱骇鍝佷俊鎭�')
+ return
+ }
+ const findLabel = (list, value) => (list || []).find(i => String(i.value) === String(value))?.label
+ const teamList = (form.value.teamList || []).map(t => ({
+ userId: t.memberId,
+ userName: findLabel(userOptions.value, t.memberId),
+ userRoleId: t.roleId,
+ userRoleName: findLabel(roleOptions.value, t.roleId),
+ joinTime: t.enterDate,
+ departTime: t.leaveDate,
+ contact: t.phone,
+ remark: t.remark
+ }))
+
+ const shippingRow = (form.value.addressList || [])[0] || {}
+ const shippingAddress = {
+ id: undefined,
+ consignee: shippingRow.receiver,
+ contract: shippingRow.phone,
+ address: shippingRow.address
+ }
+
+ const contractInfo = {
+ id: undefined,
+ name: form.value.contactName,
+ sex: form.value.contactGender === '1' ? '鐢�' : form.value.contactGender === '2' ? '濂�' : '',
+ birthday: form.value.contactBirthday,
+ department: form.value.contactDept,
+ job: form.value.contactJob,
+ phoneNumber: form.value.contactMobile,
+ email: form.value.contactEmail,
+ qq: form.value.contactQq,
+ lineaFissa: form.value.contactWorkWechat,
+ wx: form.value.contactWechat,
+ origineEtnica: form.value.contactAddress,
+ rappresentanteLegale: form.value.contactRemark
+ }
+ const info = {
+ id: form.value.id ?? null,
+ no: form.value.billNo,
+ title: form.value.projectName,
+ clientId: form.value.clientId ?? null,
+ clientName: form.value.customerName,
+ projectManagementInfoParentId: form.value.parentProjectId ?? null,
+ projectManagementPlanId: form.value.projectManagementPlanId ?? null,
+ establishTime: form.value.setupDate,
+ source: form.value.projectSource,
+ managerId: form.value.managerId ?? null,
+ managerName: form.value.creatorName,
+ salesmanId: form.value.salesmanId ?? null,
+ salesmanName: form.value.salesmanName ?? '',
+ planStartTime: form.value.planStartDate,
+ planEndTime: form.value.planEndDate,
+ actualStartTime: form.value.actualStartDate,
+ actualEndTime: form.value.actualEndDate,
+ status: form.value.billStatus === '' || form.value.billStatus === undefined || form.value.billStatus === null ? null : Number(form.value.billStatus),
+ departmentId: form.value.departmentId ?? null,
+ departmentName: form.value.departmentName ?? '',
+ orderDate: form.value.orderDate,
+ orderAmount: form.value.projectAmount,
+ reviewStatus: form.value.auditStatus === '' || form.value.auditStatus === undefined || form.value.auditStatus === null ? null : Number(form.value.auditStatus),
+ stage: form.value.projectStage === '' || form.value.projectStage === undefined || form.value.projectStage === null ? null : Number(form.value.projectStage),
+ remark: form.value.remark,
+ attachmentIds: Array.isArray(form.value.attachmentIds) ? form.value.attachmentIds : [],
+ teamList
+ }
+
+ const payload = {
+ info,
+ shippingAddress,
+ contractInfo,
+ salesLedgerProductList: productData.value
+ }
+
+ const req = operationType.value === 'edit' ? updateProject : addProject
+ const res = await req(payload)
+ if (res?.code === 200) {
+ ElMessage.success('淇濆瓨鎴愬姛')
+ closeDialog()
+ emit('completed')
+ return
+ }
+ ElMessage.error(res?.msg || '淇濆瓨澶辫触')
+}
+
+defineExpose({ openDialog })
+</script>
+
+<style scoped lang="scss">
+.section {
+ border: 1px solid #ebeef5;
+ border-radius: 8px;
+ margin-bottom: 14px;
+ background: #fff;
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 14px;
+ cursor: pointer;
+}
+
+.section-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: 600;
+ color: #303133;
+}
+
+.section-bar {
+ width: 3px;
+ height: 14px;
+ background: #e61e1e;
+ border-radius: 2px;
+}
+
+.section-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.toggle-icon {
+ color: #909399;
+}
+
+.section-body {
+ padding: 0 14px 14px;
+}
+
+.dialog-footer {
+ display: flex;
+ justify-content: center;
+ gap: 12px;
+}
+.attachment-upload{
+
+}
+</style>
--
Gitblit v1.9.3