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