spring
9 小时以前 b3af089315a98903163a394a3b1ca0e4c634b9ab
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-management into dev_New
已修改7个文件
1047 ■■■■ 文件已修改
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/dimission/components/formDia.vue 273 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/indicatorStats/index.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/receiptPaymentLedger/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesQuotation/index.vue 421 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue
@@ -32,7 +32,7 @@
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row>
                <el-row v-if="!isQuotationApproval">
                    <el-col :span="24">
                        <el-form-item label="审批事由:" prop="approveReason">
                            <el-input v-model="form.approveReason" placeholder="请输入" clearable type="textarea" disabled/>
@@ -73,6 +73,54 @@
                    </el-col>
                </el-row>
            </el-form>
      <!-- 报价审批:展示报价详情(复用销售报价“查看详情对话框”内容结构) -->
      <div v-if="isQuotationApproval" style="margin: 10px 0 18px;">
        <el-divider content-position="left">报价详情</el-divider>
        <el-skeleton :loading="quotationLoading" animated>
          <template #template>
            <el-skeleton-item variant="h3" style="width: 30%" />
            <el-skeleton-item variant="text" style="width: 100%" />
            <el-skeleton-item variant="text" style="width: 100%" />
          </template>
          <template #default>
            <el-empty v-if="!currentQuotation || !currentQuotation.quotationNo" description="未查询到对应报价详情" />
            <template v-else>
              <el-descriptions :column="2" border>
                <el-descriptions-item label="报价单号">{{ currentQuotation.quotationNo }}</el-descriptions-item>
                <el-descriptions-item label="客户名称">{{ currentQuotation.customer }}</el-descriptions-item>
                <el-descriptions-item label="业务员">{{ currentQuotation.salesperson }}</el-descriptions-item>
                <el-descriptions-item label="报价日期">{{ currentQuotation.quotationDate }}</el-descriptions-item>
                <el-descriptions-item label="有效期至">{{ currentQuotation.validDate }}</el-descriptions-item>
                <el-descriptions-item label="付款方式">{{ currentQuotation.paymentMethod }}</el-descriptions-item>
                <el-descriptions-item label="报价总额" :span="2">
                  <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
                    ¥{{ Number(currentQuotation.totalAmount ?? 0).toFixed(2) }}
                  </span>
                </el-descriptions-item>
              </el-descriptions>
              <div style="margin-top: 20px;">
                <h4>产品明细</h4>
                <el-table :data="currentQuotation.products || []" border style="width: 100%">
                  <el-table-column prop="product" label="产品名称" />
                  <el-table-column prop="specification" label="规格型号" />
                  <el-table-column prop="unit" label="单位" />
                  <el-table-column prop="unitPrice" label="单价">
                    <template #default="scope">¥{{ Number(scope.row.unitPrice ?? 0).toFixed(2) }}</template>
                  </el-table-column>
                </el-table>
              </div>
              <div v-if="currentQuotation.remark" style="margin-top: 20px;">
                <h4>备注</h4>
                <p>{{ currentQuotation.remark }}</p>
              </div>
            </template>
          </template>
        </el-skeleton>
      </div>
      <el-form :model="{ activities }" ref="formRef" label-position="top">
        <el-steps :active="getActiveStep()" finish-status="success" process-status="process" align-center direction="vertical">
          <el-step
@@ -130,7 +178,7 @@
</template>
<script setup>
import { getCurrentInstance, reactive, ref, toRefs } from "vue";
import { computed, getCurrentInstance, reactive, ref, toRefs } from "vue";
import {
    approveProcessDetails,
    getDept,
@@ -139,8 +187,16 @@
import useUserStore from "@/store/modules/user.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
import { WarningFilled, Edit, Check, MoreFilled } from '@element-plus/icons-vue'
import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
const emit = defineEmits(['close'])
const { proxy } = getCurrentInstance()
const props = defineProps({
  approveType: {
    type: [Number, String],
    default: 0
  }
})
const dialogFormVisible = ref(false);
const operationType = ref('')
@@ -149,6 +205,10 @@
const userStore = useUserStore()
const productOptions = ref([]);
const userList = ref([])
const quotationLoading = ref(false)
const currentQuotation = ref({})
const isQuotationApproval = computed(() => Number(props.approveType) === 6)
const data = reactive({
    form: {
        approveTime: "",
@@ -186,11 +246,27 @@
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
  currentQuotation.value = {}
    userListNoPageByTenantId().then((res) => {
        userList.value = res.data;
    });
    form.value = {...row}
    getProductOptions()
  // 报价审批:用审批事由字段承载的“报价单号”去查报价列表
  if (isQuotationApproval.value) {
    const quotationNo = row?.approveReason;
    if (quotationNo) {
      quotationLoading.value = true
      getQuotationList({ quotationNo }).then((res) => {
        const records = res?.data?.records || []
        currentQuotation.value = records[0] || {}
      }).finally(() => {
        quotationLoading.value = false
      })
    }
  }
  approveProcessDetails(row.approveId).then((res) => {
    activities.value = res.data
    // 增加isApproval字段
@@ -230,6 +306,8 @@
const closeDia = () => {
  proxy.resetForm("formRef");
  dialogFormVisible.value = false;
  quotationLoading.value = false
  currentQuotation.value = {}
  emit('close')
};
defineExpose({
src/views/collaborativeApproval/approvalProcess/index.vue
@@ -35,7 +35,7 @@
        >
      </div>
      <div>
        <el-button type="primary" @click="openForm('add')">新增</el-button>
        <el-button type="primary" @click="openForm('add')" v-if="currentApproveType !== 6">新增</el-button>
        <el-button @click="handleOut">导出</el-button>
        <el-button type="danger" plain @click="handleDelete">删除</el-button>
      </div>
@@ -54,7 +54,7 @@
      ></PIMTable>
    </div>
    <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="currentApproveType"></info-form-dia>
    <approval-dia ref="approvalDia" @close="handleQuery"></approval-dia>
    <approval-dia ref="approvalDia" @close="handleQuery" :approveType="currentApproveType"></approval-dia>
    <FileList ref="fileListRef" />
  </div>
</template>
@@ -103,6 +103,7 @@
const tableColumnCopy = computed(() => {
  const isLeaveType = currentApproveType.value === 2; // 请假管理
  const isReimburseType = currentApproveType.value === 4; // 报销管理
  const isQuotationType = currentApproveType.value === 6; // 报价审批
  
  // 基础列配置
  const baseColumns = [
@@ -149,7 +150,7 @@
      width: 220
    },
    {
      label: "审批事由",
      label: isQuotationType ? "报价单号" : "审批事由",
      prop: "approveReason",
      width: 200
    },
@@ -204,7 +205,7 @@
        clickFun: (row) => {
          openForm("edit", row);
        },
        disabled: (row) => row.approveStatus == 2 || row.approveStatus == 1 || row.approveStatus == 4
        disabled: (row) => currentApproveType.value === 6 || row.approveStatus == 2 || row.approveStatus == 1 || row.approveStatus == 4
      },
      {
        name: "审核",
src/views/personnelManagement/dimission/components/formDia.vue
@@ -9,10 +9,10 @@
      <!-- 员工信息展示区域 -->
      <div class="info-section">
        <div class="info-title">员工信息</div>
        <el-form :model="form" label-width="200px" label-position="left" :rules="rules" ref="formRef" style="margin-top: 20px">
        <el-row :gutter="30">
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">姓名:</span>
              <el-form-item label="姓名:" prop="staffName">
              <el-select v-model="form.staffName" placeholder="请选择人员" style="width: 100%" @change="handleSelect">
                <el-option
                  v-for="item in personList"
@@ -21,133 +21,140 @@
                  :value="item.staffName"
                />
              </el-select>
            </div>
          </el-col>
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">员工编号:</span>
              <span class="info-value">{{ form.staffNo || '-' }}</span>
            </div>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">性别:</span>
              <span class="info-value">{{ form.sex || '-' }}</span>
            </div>
          </el-col>
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">户籍住址:</span>
              <span class="info-value">{{ form.nativePlace || '-' }}</span>
            </div>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">岗位:</span>
              <span class="info-value">{{ form.postJob || '-' }}</span>
            </div>
          </el-col>
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">现住址:</span>
              <span class="info-value">{{ form.adress || '-' }}</span>
            </div>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">第一学历:</span>
              <span class="info-value">{{ form.firstStudy || '-' }}</span>
            </div>
          </el-col>
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">专业:</span>
              <span class="info-value">{{ form.profession || '-' }}</span>
            </div>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">年龄:</span>
              <span class="info-value">{{ form.age || '-' }}</span>
            </div>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">联系电话:</span>
              <span class="info-value">{{ form.phone || '-' }}</span>
            </div>
          </el-col>
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">紧急联系人:</span>
              <span class="info-value">{{ form.emergencyContact || '-' }}</span>
            </div>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">紧急联系人联系电话:</span>
              <span class="info-value">{{ form.emergencyContactPhone || '-' }}</span>
            </div>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">合同开始日期:</span>
              <span class="info-value">{{ form.contractStartTime || '-' }}</span>
            </div>
          </el-col>
          <el-col :span="12">
            <div class="info-item">
              <span class="info-label">合同结束日期:</span>
              <span class="info-value">{{ form.contractEndTime || '-' }}</span>
            </div>
          </el-col>
        </el-row>
      </div>
      <!-- 离职信息填写区域 -->
      <!-- <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef" style="margin-top: 20px">
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="离职日期:" prop="dimissionDate">
              <el-date-picker
                v-model="form.dimissionDate"
                type="date"
                placeholder="请选择离职日期"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                clearable
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
              <el-form-item label="员工编号:">
                {{ form.staffNo || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="性别:">
                {{ form.sex || '-' }}
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="户籍住址:">
                {{ form.nativePlace || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="岗位:">
                {{ form.postJob || '-' }}
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="现住址:">
                {{ form.adress || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="第一学历:">
                {{ form.firstStudy || '-' }}
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="专业:">
                {{ form.profession || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="年龄:">
                {{ form.age || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="联系电话:">
                {{ form.phone || '-' }}
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="紧急联系人:">
                {{ form.emergencyContact || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="紧急联系人联系电话:">
                {{ form.emergencyContactPhone || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
              <el-form-item label="合同开始日期:">
                {{ form.contractStartTime || '-' }}
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="合同结束日期:">
                {{ form.contractEndTime || '-' }}
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="30">
            <el-col :span="12">
            <el-form-item label="离职原因:" prop="dimissionReason">
                <el-select v-model="form.dimissionReason" placeholder="请选择离职原因" style="width: 100%" @change="handleSelectDimissionReason">
                  <el-option
                      v-for="(item, index) in dimissionReasonOptions"
                      :key="index"
                      :label="item.label"
                      :value="item.value"
                  />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="备注:" prop="dimissionRemark" v-show="form.dimissionReason === 'other'">
              <el-input
                v-model="form.dimissionReason"
                    v-model="form.dimissionRemark"
                type="textarea"
                    v-show="form.dimissionReason === 'other'"
                :rows="3"
                placeholder="请输入离职原因"
                    placeholder="备注"
                maxlength="500"
                show-word-limit
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form> -->
        </el-form>
<!--        <el-row :gutter="30">-->
<!--          <el-col :span="12">-->
<!--            <div class="info-item">-->
<!--              <span class="info-label">离职原因:</span>-->
<!--              <el-select v-model="form.dimissionReason" placeholder="请选择人员" style="width: 100%" @change="handleSelect">-->
<!--                <el-option-->
<!--                    v-for="(item, index) in dimissionReasonOptions"-->
<!--                    :key="index"-->
<!--                    :label="item.label"-->
<!--                    :value="item.value"-->
<!--                />-->
<!--              </el-select>-->
<!--            </div>-->
<!--          </el-col>-->
<!--          <el-col :span="12">-->
<!--            <div class="info-item">-->
<!--              <span class="info-label">员工编号:</span>-->
<!--              <span class="info-value">{{ form.staffNo || '-' }}</span>-->
<!--            </div>-->
<!--          </el-col>-->
<!--        </el-row>-->
      </div>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确认</el-button>
@@ -186,15 +193,22 @@
    contractEndTime: "",
    dimissionDate: "",
    dimissionReason: "",
    dimissionRemark: "",
    staffState: "",
  },
  rules: {
    staffName: [{ required: true, message: "请选择人员", trigger: "change" }],
    dimissionDate: [{ required: true, message: "请选择离职日期", trigger: "change" }],
    dimissionReason: [{ required: true, message: "请输入离职原因", trigger: "blur" }],
    staffName: [{ required: true, message: "请选择人员" }],
    dimissionReason: [{ required: true, message: "请选择离职原因"}],
  },
  dimissionReasonOptions: [
      {label: '薪资待遇', value: 'salary'},
      {label: '职业发展', value: 'career_development'},
      {label: '工作环境', value: 'work_environment'},
      {label: '个人原因', value: 'personal_reason'},
      {label: '其他', value: 'other'},
  ]
});
const { form, rules } = toRefs(data);
const { form, rules, dimissionReasonOptions } = toRefs(data);
// 打开弹框
const openDialog = (type, row) => {
@@ -207,14 +221,20 @@
    })
  }
}
const handleSelectDimissionReason = (val) => {
  if (val === 'other') {
    form.value.dimissionRemark = ''
  }
}
// 提交产品表单
const submitForm = () => {
  // 表单已注释,直接提交,不进行验证
  if (!form.value.staffName) {
    proxy.$modal.msgError("请选择人员");
    return;
  }
  form.value.staffState = 0
  if (form.value.dimissionReason !== 'other') {
    form.value.dimissionRemark = ''
  }
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
  if (operationType.value === "add") {
    staffJoinAdd(form.value).then(res => {
      proxy.$modal.msgSuccess("提交成功");
@@ -227,6 +247,9 @@
    })
  }
}
  })
}
// 关闭弹框
const closeDia = () => {
  // 表单已注释,手动重置表单数据
src/views/salesManagement/indicatorStats/index.vue
@@ -3,7 +3,7 @@
    <el-card class="box-card">
      <!-- KPI 汇总 -->
      <el-row :gutter="20" class="stats-row">
        <el-col :span="6">
        <el-col :span="8">
          <div class="stat-card">
            <div class="stat-icon" style="background: #ecf5ff;">
              <el-icon :size="30" color="#409eff"><Document /></el-icon>
@@ -14,7 +14,7 @@
            </div>
          </div>
        </el-col>
        <el-col :span="6">
        <el-col :span="8">
          <div class="stat-card">
            <div class="stat-icon" style="background: #f0f9ff;">
              <el-icon :size="30" color="#67c23a"><Tickets /></el-icon>
@@ -25,7 +25,7 @@
            </div>
          </div>
        </el-col>
        <el-col :span="6">
        <el-col :span="8">
          <div class="stat-card">
            <div class="stat-icon" style="background: #fef0f0;">
              <el-icon :size="30" color="#e6a23c"><Van /></el-icon>
@@ -33,17 +33,6 @@
            <div class="stat-content">
              <div class="stat-value">{{ indicatorKpis.shipmentRate }}%</div>
              <div class="stat-label">发货率</div>
            </div>
          </div>
        </el-col>
        <el-col :span="6">
          <div class="stat-card">
            <div class="stat-icon" style="background: #f4f4f5;">
              <el-icon :size="30" color="#f56c6c"><Wallet /></el-icon>
            </div>
            <div class="stat-content">
              <div class="stat-value">{{ indicatorKpis.collectionRate }}%</div>
              <div class="stat-label">回款率</div>
            </div>
          </div>
        </el-col>
@@ -93,7 +82,7 @@
      </div>
      <!-- 业绩统计(团队维度,无个人姓名) -->
      <el-table :data="teamPerformanceList" border stripe style="margin-top: 20px;">
      <el-table v-if="showTeamPerformance" :data="teamPerformanceList" border stripe style="margin-top: 20px;">
        <el-table-column prop="team" label="销售团队"/>
        <el-table-column prop="orderCount" label="订单数"/>
        <el-table-column prop="salesAmount" label="销售额">
@@ -101,9 +90,6 @@
        </el-table-column>
        <el-table-column prop="shipmentRate" label="发货率">
          <template #default="scope">{{ scope.row.shipmentRate }}%</template>
        </el-table-column>
        <el-table-column prop="collectionRate" label="回款率">
          <template #default="scope">{{ scope.row.collectionRate }}%</template>
        </el-table-column>
        <el-table-column prop="attainment" label="目标达成率">
          <template #default="scope">
@@ -119,15 +105,17 @@
<script setup>
import { ref, reactive, onMounted, nextTick } from 'vue'
import { Document, Van, Tickets, Wallet } from '@element-plus/icons-vue'
import { Document, Van, Tickets } from '@element-plus/icons-vue'
import * as echarts from 'echarts'
const indicatorKpis = reactive({
  orderCount: 1280,
  salesAmount: 9650000,
  shipmentRate: 89.2,
  collectionRate: 76.4
  shipmentRate: 89.2
})
// 是否展示销售团队明细表,按需开启
const showTeamPerformance = ref(false)
const indicatorFilter = reactive({
  product: '',
@@ -140,10 +128,10 @@
let indicatorChart = null
const teamPerformanceList = ref([
  { team: '华东大区', orderCount: 320, salesAmount: 2850000, shipmentRate: 90, collectionRate: 80, attainment: 105 },
  { team: '华北大区', orderCount: 280, salesAmount: 2150000, shipmentRate: 86, collectionRate: 73, attainment: 92 },
  { team: '华南大区', orderCount: 210, salesAmount: 1850000, shipmentRate: 88, collectionRate: 70, attainment: 78 },
  { team: '西南大区', orderCount: 180, salesAmount: 1500000, shipmentRate: 83, collectionRate: 68, attainment: 74 }
  { team: '华东大区', orderCount: 320, salesAmount: 2850000, shipmentRate: 90, attainment: 105 },
  { team: '华北大区', orderCount: 280, salesAmount: 2150000, shipmentRate: 86, attainment: 92 },
  { team: '华南大区', orderCount: 210, salesAmount: 1850000, shipmentRate: 88, attainment: 78 },
  { team: '西南大区', orderCount: 180, salesAmount: 1500000, shipmentRate: 83, attainment: 74 }
])
const initIndicatorChart = () => {
@@ -153,7 +141,7 @@
  const option = {
    title: { text: '多维度销售指标趋势', left: 'center' },
    tooltip: { trigger: 'axis' },
    legend: { data: ['订单数', '销售额', '发货率', '回款率'], top: 30 },
    legend: { data: ['订单数', '销售额', '发货率'], top: 30 },
    grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
    xAxis: { type: 'category', data: ['2024-12', '2025-01', '2025-02', '2025-03', '2025-04', '2025-05'] },
    yAxis: [
@@ -163,8 +151,7 @@
    series: [
      { name: '订单数', type: 'bar', data: [180, 220, 210, 260, 205, 225], itemStyle: { color: '#409eff' } },
      { name: '销售额', type: 'bar', data: [820, 950, 910, 1080, 980, 1020], itemStyle: { color: '#67c23a' } },
      { name: '发货率', type: 'line', yAxisIndex: 1, data: [86, 89, 88, 91, 87, 90], itemStyle: { color: '#e6a23c' } },
      { name: '回款率', type: 'line', yAxisIndex: 1, data: [72, 76, 74, 79, 75, 78], itemStyle: { color: '#f56c6c' } }
      { name: '发货率', type: 'line', yAxisIndex: 1, data: [86, 89, 88, 91, 87, 90], itemStyle: { color: '#e6a23c' } }
    ]
  }
  indicatorChart.setOption(option)
@@ -178,7 +165,6 @@
  indicatorKpis.orderCount = random(1280, 120)
  indicatorKpis.salesAmount = random(9650000, 350000)
  indicatorKpis.shipmentRate = (85 + Math.random() * 10).toFixed(1) * 1
  indicatorKpis.collectionRate = (70 + Math.random() * 12).toFixed(1) * 1
  setTimeout(() => initIndicatorChart(), 200)
}
@@ -191,13 +177,12 @@
}
const exportIndicatorTable = () => {
  const header = ['销售团队', '订单数', '销售额', '发货率(%)', '回款率(%)', '目标达成率(%)']
  const header = ['销售团队', '订单数', '销售额', '发货率(%)', '目标达成率(%)']
  const rows = teamPerformanceList.value.map(r => [
    r.team,
    r.orderCount,
    r.salesAmount,
    r.shipmentRate,
    r.collectionRate,
    r.attainment
  ])
  const csv = [header, ...rows].map(r => r.join(',')).join('\n')
src/views/salesManagement/receiptPaymentLedger/index.vue
@@ -41,7 +41,7 @@
                        width="200"
          />
          <el-table-column
            label="开票金额(元)"
            label="合同金额(元)"
            prop="invoiceTotal"
            show-overflow-tooltip
            :formatter="formattedNumber"
@@ -93,33 +93,39 @@
          />
          <el-table-column
            label="发生日期"
            prop="happenTime"
            prop="receiptPaymentDate"
            show-overflow-tooltip
                        width="110"
          />
          <el-table-column
            label="开票金额(元)"
            prop="invoiceAmount"
            label="销售合同号"
            prop="salesContractNo"
            show-overflow-tooltip
                        width="200"
          />
          <el-table-column
            label="合同金额(元)"
            prop="invoiceTotal"
            show-overflow-tooltip
            :formatter="formattedNumber"
                        width="200"
          />
          <el-table-column
            label="回款金额(元)"
            prop="receiptAmount"
            prop="receiptPaymentAmount"
            show-overflow-tooltip
            :formatter="formattedNumber"
                        width="200"
          />
          <el-table-column
            label="应收金额(元)"
            prop="unReceiptAmount"
            prop="unReceiptPaymentAmount"
            show-overflow-tooltip
                        width="200"
          >
            <template #default="{ row, column }">
              <el-text type="danger">
                {{ formattedNumber(row, column, row.unReceiptAmount) }}
                {{ formattedNumber(row, column, row.unReceiptPaymentAmount) }}
              </el-text>
            </template>
          </el-table-column>
src/views/salesManagement/salesLedger/index.vue
@@ -98,6 +98,11 @@
      @confirm="submitForm"
      @cancel="closeDia">
      <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
        <el-row v-if="operationType !== 'view'">
          <el-col :span="24" style="display:flex; justify-content:flex-end; gap:10px; margin-bottom: 6px;">
            <el-button type="primary" plain @click="openQuotationDialog">从审批通过的报价单导入</el-button>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="销售合同号:" prop="salesContractNo">
@@ -203,6 +208,62 @@
        </el-row>
      </el-form>
    </FormDialog>
    <!-- 从报价单导入(仅审批通过) -->
    <el-dialog
      v-model="quotationDialogVisible"
      title="选择审批通过的销售报价单"
      width="80%"
      :close-on-click-modal="false"
    >
      <div style="margin-bottom: 12px; display:flex; gap: 12px; align-items:center;">
        <el-input
          v-model="quotationSearchForm.quotationNo"
          placeholder="请输入报价单号"
          clearable
          style="max-width: 260px;"
          @change="fetchQuotationList"
        />
        <el-input
          v-model="quotationSearchForm.customer"
          placeholder="请输入客户名称"
          clearable
          style="max-width: 260px;"
          @change="fetchQuotationList"
        />
        <el-button type="primary" @click="fetchQuotationList">搜索</el-button>
        <el-button @click="resetQuotationSearch">重置</el-button>
      </div>
      <el-table
        :data="quotationList"
        border
        stripe
        v-loading="quotationLoading"
        height="420px"
      >
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column prop="quotationNo" label="报价单号" width="180" show-overflow-tooltip />
        <el-table-column prop="customer" label="客户名称" min-width="220" show-overflow-tooltip />
        <el-table-column prop="salesperson" label="业务员" width="120" show-overflow-tooltip />
        <el-table-column prop="quotationDate" label="报价日期" width="140" />
        <el-table-column prop="status" label="审批状态" width="120" align="center" />
        <el-table-column prop="totalAmount" label="报价金额(元)" width="160" align="right">
          <template #default="scope">
            {{ Number(scope.row.totalAmount ?? 0).toFixed(2) }}
          </template>
        </el-table-column>
        <el-table-column fixed="right" label="操作" width="120" align="center">
          <template #default="scope">
            <el-button type="primary" link @click="applyQuotation(scope.row)">选择</el-button>
          </template>
        </el-table-column>
      </el-table>
      <template #footer>
        <el-button @click="quotationDialogVisible = false">关闭</el-button>
      </template>
    </el-dialog>
    <FormDialog 
      v-model="productFormVisible" 
      :title="productOperationType === 'add' ? '新增产品' : '编辑产品'" 
@@ -461,6 +522,7 @@
import { userListNoPage } from "@/api/system/user.js";
import FileListDialog from '@/components/Dialog/FileListDialog.vue';
import FormDialog from '@/components/Dialog/FormDialog.vue';
import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
import {
    ledgerListPage,
    productList,
@@ -576,6 +638,16 @@
const printPreviewVisible = ref(false);
const printData = ref([]);
// 报价单导入相关
const quotationDialogVisible = ref(false);
const quotationLoading = ref(false);
const quotationList = ref([]);
const quotationSearchForm = reactive({
  quotationNo: "",
  customer: "",
});
const selectedQuotation = ref(null);
// 发货相关
const deliveryFormVisible = ref(false);
const currentDeliveryRow = ref(null);
@@ -644,8 +716,10 @@
};
// 获取产品大类tree数据
const getProductOptions = () => {
  productTreeList().then((res) => {
  // 返回 Promise,便于在编辑产品时等待加载完成
  return productTreeList().then((res) => {
    productOptions.value = convertIdToValue(res);
    return productOptions.value;
  });
};
const formattedNumber = (row, column, cellValue) => {
@@ -695,6 +769,19 @@
    return newItem;
  });
}
// 根据名称反查产品大类 id,便于仅存名称时的反显
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;
}
// 表格选择数据
const handleSelectionChange = (selection) => {
@@ -746,6 +833,7 @@
  operationType.value = type;
  form.value = {};
  productData.value = [];
  selectedQuotation.value = null;
  let userLists = await userListNoPage();
  userList.value = userLists.data;
  customerList().then((res) => {
@@ -774,6 +862,87 @@
  // });
  form.value.entryDate = getCurrentDate(); // 设置默认录入日期为当前日期
  dialogFormVisible.value = true;
};
// 打开报价单选择弹窗(仅审批通过)
const openQuotationDialog = async () => {
  if (operationType.value === "view") return;
  quotationDialogVisible.value = true;
  // 先确保客户列表已加载,便于后续回填 customerId
  if (!customerOption.value || customerOption.value.length === 0) {
    try {
      const res = await customerList();
      customerOption.value = res;
    } catch (e) {
      // ignore,允许用户后续手动选择客户
    }
  }
  await fetchQuotationList();
};
const fetchQuotationList = async () => {
  quotationLoading.value = true;
  try {
    const params = {
      // 兼容后端分页字段:这里沿用报价页面已有可用的字段命名
      currentPage: 1,
      pageSize: 100,
      ...quotationSearchForm,
      status: "通过",
    };
    const res = await getQuotationList(params);
    quotationList.value = res?.data?.records || [];
  } finally {
    quotationLoading.value = false;
  }
};
const resetQuotationSearch = async () => {
  quotationSearchForm.quotationNo = "";
  quotationSearchForm.customer = "";
  await fetchQuotationList();
};
// 选中报价单后回填到台账表单
const applyQuotation = (row) => {
  if (!row) return;
  selectedQuotation.value = row;
  // 业务员
  form.value.salesman = row.salesperson || "";
  // 客户名称 -> customerId
  const customer = (customerOption.value || []).find((c) => c.customerName === row.customer);
  if (customer?.id) {
    form.value.customerId = customer.id;
  } else {
    // 如果找不到,保留原值(允许用户手动选择/不打断已有输入)
    form.value.customerId = form.value.customerId || "";
  }
  // 产品信息映射:报价 products -> 台账 productData
  const products = Array.isArray(row.products) ? row.products : [];
  productData.value = products.map((p) => {
    const quantity = Number(p.quantity ?? 0) || 0;
    const unitPrice = Number(p.unitPrice ?? 0) || 0;
    const taxRate = "13"; // 默认 13%,便于直接提交(如需可在产品中自行修改)
    const taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2);
    const taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(taxInclusiveTotalPrice, taxRate);
    return {
      // 台账字段
      productCategory: p.product || p.productName || "",
      specificationModel: p.specification || "",
      unit: p.unit || "",
      quantity: quantity,
      taxRate: taxRate,
      taxInclusiveUnitPrice: unitPrice.toFixed(2),
      taxInclusiveTotalPrice: taxInclusiveTotalPrice,
      taxExclusiveTotalPrice: taxExclusiveTotalPrice,
      invoiceType: "增普票",
    };
  });
  quotationDialogVisible.value = false;
};
function changs(val) {
  console.log(val);
@@ -847,16 +1016,36 @@
const productIndex = ref(0);
// 打开产品弹框
const openProductForm = (type, row,index) => {
const openProductForm = async (type, row, index) => {
  productOperationType.value = type;
  productForm.value = {};
  proxy.resetForm("productFormRef");
  if (type === "edit") {
    productForm.value = { ...row };
    productIndex.value = index;
    // 编辑时根据产品大类名称反查 tree 节点 id,并加载规格型号列表
    try {
      const options = productOptions.value && productOptions.value.length > 0
        ? productOptions.value
        : await getProductOptions();
      const categoryId = findNodeIdByLabel(options, productForm.value.productCategory);
      if (categoryId) {
        const models = await modelList({ id: categoryId });
        modelOptions.value = models || [];
        // 根据当前规格型号名称反查并设置 productModelId,便于下拉框显示已选值
        const currentModel = (modelOptions.value || []).find(
          (m) => m.model === productForm.value.specificationModel
        );
        if (currentModel) {
          productForm.value.productModelId = currentModel.id;
        }
      }
    } catch (e) {
      // 加载失败时保持可编辑,不中断弹窗
      console.error("加载产品规格型号失败", e);
    }
  }
  productFormVisible.value = true;
  getProductOptions();
};
// 提交产品表单
const submitProduct = () => {
src/views/salesManagement/salesQuotation/index.vue
@@ -56,16 +56,23 @@
        <el-table-column prop="salesperson" label="业务员" width="100" />
        <el-table-column prop="quotationDate" label="报价日期" width="120" />
        <el-table-column prop="validDate" label="有效期至" width="120" />
        <el-table-column prop="status" label="审批状态" width="120" align="center">
          <template #default="{ row }">
            <el-tag :type="getStatusType(row.status)" disable-transitions>
              {{ row.status || '--' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="totalAmount" label="报价金额" width="120">
          <template #default="scope">
            ¥{{ scope.row.totalAmount.toFixed(2) }}
          </template>
        </el-table-column>
        <el-table-column label="操作" width="250" fixed="right" align="center">
        <el-table-column label="操作" width="200" fixed="right" align="center">
          <template #default="scope">
            <el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
            <el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.status === '草稿'">编辑</el-button>
            <el-button link type="danger" @click="handleDelete(scope.row)" v-if="scope.row.status === '草稿'">删除</el-button>
            <el-button link type="primary" @click="handleEdit(scope.row)" :disabled="!['待审批','拒绝'].includes(scope.row.status)">编辑</el-button>
            <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
@@ -81,17 +88,22 @@
    </el-card>
    <!-- 新增/编辑对话框 -->
    <FormDialog v-model="dialogVisible" :title="dialogTitle" width="80%" :close-on-click-modal="false" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
    <FormDialog v-model="dialogVisible" :title="dialogTitle" width="85%" :close-on-click-modal="false" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
      <div class="quotation-form-container">
        <el-form :model="form" :rules="rules" ref="formRef" label-width="120px" class="quotation-form">
        <!-- 基本信息 -->
        <el-card class="form-card" shadow="never">
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><Document /></el-icon>
            <span class="card-title">基本信息</span>
            </div>
          </template>
          <el-row :gutter="20">
          <div class="form-content">
            <el-row :gutter="24">
            <el-col :span="12">
              <el-form-item label="客户名称" prop="customer">
                <el-select v-model="form.customer" placeholder="请选择客户" style="width: 100%" @change="handleCustomerChange">
                  <el-select v-model="form.customer" placeholder="请选择客户" style="width: 100%" @change="handleCustomerChange" clearable>
                                    <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.customerName">
                                        {{
                                            item.customerName + "——" + item.taxpayerIdentificationNumber
@@ -102,14 +114,14 @@
            </el-col>
            <el-col :span="12">
              <el-form-item label="业务员" prop="salesperson">
                <el-select v-model="form.salesperson" placeholder="请选择业务员" style="width: 100%">
                  <el-select v-model="form.salesperson" placeholder="请选择业务员" style="width: 100%" clearable>
                                    <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName"
                                                         :value="item.nickName" />
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-row :gutter="24">
            <el-col :span="12">
              <el-form-item label="报价日期" prop="quotationDate">
                <el-date-picker
@@ -119,6 +131,7 @@
                  style="width: 100%"
                  format="YYYY-MM-DD"
                  value-format="YYYY-MM-DD"
                    clearable
                />
              </el-form-item>
            </el-col>
@@ -131,32 +144,91 @@
                  style="width: 100%"
                  format="YYYY-MM-DD"
                  value-format="YYYY-MM-DD"
                    clearable
                />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-row :gutter="24">
            <el-col :span="12">
              <el-form-item label="付款方式" prop="paymentMethod">
                <el-select v-model="form.paymentMethod" placeholder="请选择付款方式" style="width: 100%">
                  <el-option label="全款到付" value="全款到付"></el-option>
                  <el-option label="分期付款" value="分期付款"></el-option>
                  <el-option label="月结" value="月结"></el-option>
                </el-select>
                  <el-input v-model="form.paymentMethod" placeholder="请输入付款方式" clearable />
              </el-form-item>
            </el-col>
          </el-row>
          </div>
        </el-card>
        <!-- 审批人信息 -->
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><UserFilled /></el-icon>
              <span class="card-title">审批人选择</span>
              <el-button type="primary" size="small" @click="addApproverNode" class="header-btn">
                <el-icon><Plus /></el-icon>
                新增节点
              </el-button>
            </div>
          </template>
          <div class="form-content">
            <el-row>
              <el-col :span="24">
                <el-form-item>
                  <div class="approver-nodes-container">
                    <div
                      v-for="(node, index) in approverNodes"
                      :key="node.id"
                      class="approver-node-item"
                    >
                      <div class="approver-node-label">
                        <span class="node-step">{{ index + 1 }}</span>
                        <span class="node-text">审批人</span>
                        <el-icon class="arrow-icon"><ArrowRight /></el-icon>
                      </div>
                      <el-select
                        v-model="node.userId"
                        placeholder="选择人员"
                        class="approver-select"
                        clearable
                      >
                        <el-option
                          v-for="user in userList"
                          :key="user.userId"
                          :label="user.nickName"
                          :value="user.userId"
                        />
                      </el-select>
                      <el-button
                        type="danger"
                        size="small"
                        :icon="Delete"
                        @click="removeApproverNode(index)"
                        v-if="approverNodes.length > 1"
                        class="remove-btn"
                      >删除</el-button>
                    </div>
                  </div>
                </el-form-item>
              </el-col>
            </el-row>
          </div>
        </el-card>
        <!-- 产品信息 -->
        <el-card class="form-card" shadow="never">
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header">
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><Box /></el-icon>
              <span class="card-title">产品信息</span>
              <el-button type="primary" size="small" @click="addProduct">添加产品</el-button>
              <el-button type="primary" size="small" @click="addProduct" class="header-btn">
                <el-icon><Plus /></el-icon>
                添加产品
              </el-button>
            </div>
          </template>
          <el-table :data="form.products" border style="width: 100%">
          <div class="form-content">
            <el-table :data="form.products" border style="width: 100%" class="product-table" v-if="form.products.length > 0">
            <el-table-column prop="product" label="产品名称" width="200">
              <template #default="scope">
                                <el-tree-select
@@ -204,18 +276,33 @@
              </template>
            </el-table-column>
          </el-table>
          <el-empty v-else description="暂无产品,请点击添加产品" :image-size="80" />
          </div>
        </el-card>
        <!-- 备注信息 -->
        <el-card class="form-card" shadow="never">
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><EditPen /></el-icon>
            <span class="card-title">备注信息</span>
            </div>
          </template>
          <div class="form-content">
          <el-form-item label="备注" prop="remark">
            <el-input type="textarea" v-model="form.remark" placeholder="请输入备注信息" rows="3"></el-input>
              <el-input
                type="textarea"
                v-model="form.remark"
                placeholder="请输入备注信息(选填)"
                :rows="4"
                maxlength="500"
                show-word-limit
              ></el-input>
          </el-form-item>
          </div>
        </el-card>
      </el-form>
      </div>
    </FormDialog>
    <!-- 查看详情对话框 -->
@@ -260,7 +347,7 @@
<script setup>
import { ref, reactive, computed, onMounted, markRaw, shallowRef } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import { Search, Document, UserFilled, Box, EditPen, Plus, ArrowRight, Delete } from '@element-plus/icons-vue'
import Pagination from '@/components/PIMTable/Pagination.vue'
import FormDialog from '@/components/Dialog/FormDialog.vue'
import {getQuotationList,addQuotation,updateQuotation,deleteQuotation} from '@/api/salesManagement/salesQuotation.js'
@@ -311,15 +398,31 @@
  salesperson: [{ required: true, message: '请选择业务员', trigger: 'change' }],
  quotationDate: [{ required: true, message: '请选择报价日期', trigger: 'change' }],
  validDate: [{ required: true, message: '请选择有效期', trigger: 'change' }],
  paymentMethod: [{ required: true, message: '请选择付款方式', trigger: 'change' }]
  paymentMethod: [{ required: true, message: '请输入付款方式', trigger: 'blur' }]
}
const userList = ref([]);
const customerOption = ref([]);
// 审批人节点相关
const approverNodes = ref([
  { id: 1, userId: null }
])
let nextApproverId = 2
const isEdit = ref(false)
const editId = ref(null)
const currentQuotation = ref({})
const formRef = ref()
// 添加审批人节点
function addApproverNode() {
  approverNodes.value.push({ id: nextApproverId++, userId: null })
}
// 删除审批人节点
function removeApproverNode(index) {
  approverNodes.value.splice(index, 1)
}
// 计算属性
const filteredList = computed(() => {
@@ -339,10 +442,10 @@
// 方法
const getStatusType = (status) => {
  const statusMap = {
    '草稿': 'info',
    '已发送': 'primary',
    '客户确认': 'success',
    '已过期': 'danger'
    '待审批': 'info',
    '审核中': 'primary',
    '通过': 'success',
    '拒绝': 'danger'
  }
  return statusMap[status] || 'info'
}
@@ -357,6 +460,9 @@
  dialogTitle.value = '新增报价'
  isEdit.value = false
  resetForm()
  // 重置审批人节点
  approverNodes.value = [{ id: 1, userId: null }]
  nextApproverId = 2
  dialogVisible.value = true
    let userLists = await userListNoPage();
    // 只复制需要的字段,避免将组件引用放入响应式对象
@@ -376,8 +482,10 @@
    });
}
const getProductOptions = () => {
    productTreeList().then((res) => {
    // 返回 Promise,便于编辑时 await 确保能反显
    return productTreeList().then((res) => {
        productOptions.value = convertIdToValue(res);
        return productOptions.value
    });
};
function convertIdToValue(data) {
@@ -393,6 +501,19 @@
        
        return newItem;
    });
}
// 根据名称反查节点 id,便于仅存名称时的反显
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;
}
const getModels = (value, row) => {
    if (!row) return;
@@ -478,10 +599,14 @@
  viewDialogVisible.value = true
}
const handleEdit = (row) => {
const handleEdit = async (row) => {
  dialogTitle.value = '编辑报价'
  isEdit.value = true
  editId.value = row.id
  form.id = row.id || form.id || null
  // 先加载产品树数据,否则 el-tree-select 无法反显产品名称
  await getProductOptions()
  // 只复制需要的字段,避免将组件引用放入响应式对象
  form.quotationNo = row.quotationNo || ''
  form.customer = row.customer || ''
@@ -491,22 +616,51 @@
  form.paymentMethod = row.paymentMethod || ''
  form.status = row.status || '草稿'
  form.remark = row.remark || ''
  form.products = row.products ? row.products.map(product => ({
    productId: product.productId || '',
    product: product.product || product.productName || '',
  form.products = row.products ? row.products.map(product => {
    const productName = product.product || product.productName || ''
    // 优先用 productId;如果只有名称,尝试反查 id 以便树选择器反显
    const resolvedId = product.productId
      ? Number(product.productId)
      : findNodeIdByLabel(productOptions.value, productName) || ''
    return {
      productId: resolvedId,
      product: productName,
    specificationId: product.specificationId || '',
    specification: product.specification || '',
    quantity: product.quantity || 0,
    unit: product.unit || '',
    unitPrice: product.unitPrice || 0,
    amount: product.amount || 0
  })) : []
    }
  }) : []
  form.subtotal = row.subtotal || 0
  form.freight = row.freight || 0
  form.otherFee = row.otherFee || 0
  form.discountRate = row.discountRate || 0
  form.discountAmount = row.discountAmount || 0
  form.totalAmount = row.totalAmount || 0
  // 反显审批人
  if (row.approveUserIds) {
    const userIds = row.approveUserIds.split(',')
    approverNodes.value = userIds.map((userId, idx) => ({
      id: idx + 1,
      userId: parseInt(userId.trim())
    }))
    nextApproverId = userIds.length + 1
  } else {
    approverNodes.value = [{ id: 1, userId: null }]
    nextApproverId = 2
  }
  // 加载用户列表
  let userLists = await userListNoPage();
  userList.value = (userLists.data || []).map(item => ({
    userId: item.userId,
    nickName: item.nickName || '',
    userName: item.userName || ''
  }));
  dialogVisible.value = true
}
@@ -596,6 +750,22 @@
        return
      }
      
      // 审批人必填校验
      const hasEmptyApprover = approverNodes.value.some(node => !node.userId)
      if (hasEmptyApprover) {
        ElMessage.error('请为所有审批节点选择审批人!')
        return
      }
      // 收集所有节点的审批人id
      form.approveUserIds = approverNodes.value.map(node => node.userId).join(',')
      // 计算所有产品的单价总和
      form.totalAmount = form.products.reduce((sum, product) => {
        const price = Number(product.unitPrice) || 0
        return sum + price
      }, 0)
      if (isEdit.value) {
        // 编辑
        const index = quotationList.value.findIndex(item => item.id === editId.value)
@@ -608,30 +778,16 @@
              handleSearch()
            }
          })
          // quotationList.value[index] = { ...form, id: editId.value }
          // ElMessage.success('编辑成功')
        }
      } else {
        // 新增
        // const newId = Math.max(...quotationList.value.map(item => item.id)) + 1
        form.quotationNo = `QT${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(new Date().getDate()).padStart(2, '0')}`
        addQuotation(form).then(res=>{
          // console.log(res)
          if(res.code===200){
            ElMessage.success('新增成功')
            dialogVisible.value = false
            handleSearch()
          }
        })
        // quotationList.value.push({
        //   ...form,
        //   // id: newId,
        //   quotationNo: quotationNo
        // })
        // pagination.total++
        // ElMessage.success('新增成功')
      }
      
    }
@@ -644,7 +800,7 @@
}
const handleSearch = ()=>{
  const params = {
    page:pagination,
    ...pagination,
    ...searchForm
  }
  getQuotationList(params).then(res=>{
@@ -660,6 +816,8 @@
        validDate: item.validDate || '',
        paymentMethod: item.paymentMethod || '',
        status: item.status || '草稿',
        // 审批人(用于编辑时反显)
        approveUserIds: item.approveUserIds || '',
        remark: item.remark || '',
        products: item.products ? item.products.map(product => ({
          productId: product.productId || '',
@@ -696,27 +854,182 @@
})
</script>
<style scoped>
<style scoped lang="scss">
.search-row {
  margin-bottom: 20px;
}
.quotation-form-container {
  padding: 10px 0;
  max-height: calc(100vh - 200px);
  overflow-y: auto;
  &::-webkit-scrollbar {
    width: 6px;
    height: 6px;
  }
  &::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 3px;
    &:hover {
      background: #a8a8a8;
    }
  }
}
.quotation-form {
  .el-form-item {
    margin-bottom: 22px;
  }
}
.form-card {
  margin-bottom: 20px;
  margin-bottom: 24px;
  border-radius: 8px;
  transition: all 0.3s ease;
  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
  }
  :deep(.el-card__header) {
    padding: 16px 20px;
    background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
    border-bottom: 1px solid #ebeef5;
  }
  :deep(.el-card__body) {
    padding: 20px;
  }
}
.card-header-wrapper {
  display: flex;
  align-items: center;
  gap: 8px;
  .card-icon {
    font-size: 18px;
    color: #409eff;
}
.card-title {
  font-weight: bold;
    font-weight: 600;
    font-size: 16px;
  color: #303133;
    flex: 1;
}
.card-header {
  .header-btn {
    margin-left: auto;
  }
}
.form-content {
  padding: 8px 0;
}
.approver-nodes-container {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 24px;
  padding: 12px 0;
}
.approver-node-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 16px;
  background: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #e4e7ed;
  transition: all 0.3s ease;
  min-width: 180px;
  &:hover {
    border-color: #409eff;
    background: #f0f7ff;
    box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
  }
}
.approver-node-label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  color: #606266;
  .node-step {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    background: #409eff;
    color: #fff;
    border-radius: 50%;
    font-size: 12px;
    font-weight: 600;
  }
  .node-text {
    font-weight: 500;
  }
  .arrow-icon {
    color: #909399;
    font-size: 14px;
  }
}
.approver-select {
  width: 100%;
  min-width: 150px;
}
.remove-btn {
  margin-top: 4px;
}
.product-table {
  :deep(.el-table__header) {
    background-color: #f5f7fa;
    th {
      background-color: #f5f7fa !important;
      color: #606266;
      font-weight: 600;
    }
  }
  :deep(.el-table__row) {
    &:hover {
      background-color: #f5f7fa;
    }
  }
  :deep(.el-table__cell) {
    padding: 12px 0;
  }
}
.dialog-footer {
  text-align: right;
}
// 响应式优化
@media (max-width: 1200px) {
  .approver-nodes-container {
    gap: 16px;
  }
  .approver-node-item {
    min-width: 160px;
  }
}
</style>