gaoluyang
7 天以前 c9ed3d1958a2489460592b3b17e386d9d515d7ea
君歌
1.工序修改
2.销售报价重构
已添加2个文件
已修改5个文件
966 ■■■■ 文件已修改
src/api/salesManagement/salesQuotation.js 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesQuotationRecord.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/Edit.vue 106 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/New.vue 99 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/index.vue 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesQuotation/ImportQuotationDetail.vue 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesQuotation/index.vue 465 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/salesQuotation.js
@@ -110,3 +110,40 @@
    responseType: "blob",
  });
}
// ä¸‹è½½æŠ¥ä»·å¯¼å…¥æ¨¡æ¿
export function downloadQuotationTemplate() {
  return request({
    url: "/sales/quotation/downloadTemplate",
    method: "get",
    responseType: "blob",
  });
}
// å¯¼å…¥æŠ¥ä»·å•
export function importQuotation(data) {
  return request({
    url: "/sales/quotation/import",
    method: "post",
    data: data,
    headers: { "Content-Type": "multipart/form-data" },
  });
}
// æŸ¥è¯¢å¯¼å…¥è®°å½•列表
export function getImportLogList(query) {
  return request({
    url: "/sales/quotation/importLog/list",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢é™ä»·åŽ†å²è®°å½•
export function getPriceHistoryList(query) {
  return request({
    url: "/sales/quotation/priceHistory/list",
    method: "get",
    params: query,
  });
}
src/api/salesManagement/salesQuotationRecord.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,11 @@
// é”€å”®æŠ¥ä»·é¡µé¢æŽ¥å£
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢æŠ¥ä»·å•列表
export function getQuotationRecordList(query) {
  return request({
    url: "/sales/quotationRecord/listPage",
    method: "get",
    params: query,
  });
}
src/views/productionManagement/productionProcess/Edit.vue
@@ -1,19 +1,19 @@
<template>
  <div>
    <el-dialog
  <FormDialog
        v-model="isShow"
        title="编辑工序"
        width="400"
        @close="closeModal"
    width="800px"
    @confirm="handleSubmit"
    @cancel="closeModal"
    >
      <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
    <el-form label-width="180px" :model="formState" label-position="top" ref="formRef">
        <el-form-item
            label="工序名称:"
          label="部件:"
            prop="name"
            :rules="[
                {
                required: true,
                message: '请输入工序名称',
              message: '请输入部件',
              },
              {
                max: 100,
@@ -27,7 +27,7 @@
        </el-form-item>
        <el-form-item
            label="工序类型"
            prop="type"
          prop="processType"
            :rules="[
                {
                required: true,
@@ -35,13 +35,39 @@
              }
            ]"
        >
          <el-select v-model="formState.type" placeholder="请选择工序类型">
            <el-option label="计时" :value="0" />
            <el-option label="计件" :value="1" />
        <el-select v-model="formState.processType" placeholder="请选择工序类型" style="width: 100%">
          <el-option v-for="item in processTypeOptions"
                     :key="item"
                     :label="item"
                     :value="item" />
          </el-select>
        </el-form-item>
        <el-form-item label="工资定额" prop="salaryQuota">
          <el-input v-model="formState.salaryQuota" type="number" :step="0.001" />
      <el-form-item label="计划工时(小时)" prop="salaryQuota">
        <el-input v-model="formState.salaryQuota" type="number" :step="0.5" />
      </el-form-item>
      <el-form-item label="计划人员" prop="planPerson">
        <el-select v-model="formState.planPerson"
                   placeholder="请选择计划人员"
                   clearable
                   filterable
                   style="width: 100%">
          <el-option v-for="item in employeeOptions"
                     :key="item.id"
                     :label="item.staffName"
                     :value="item.id" />
        </el-select>
      </el-form-item>
      <el-form-item label="计划执行人员" prop="executor">
        <el-select v-model="formState.executor"
                   placeholder="请选择计划执行人员"
                   clearable
                   filterable
                   style="width: 100%">
          <el-option v-for="item in employeeOptions"
                     :key="item.id"
                     :label="item.staffName"
                     :value="item.id" />
        </el-select>
        </el-form-item>
        <el-form-item label="是否质检" prop="isQuality">
          <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/>
@@ -56,19 +82,14 @@
          <el-input v-model="formState.remark" type="textarea" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确认</el-button>
          <el-button @click="closeModal">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
  </FormDialog>
</template>
<script setup>
import { ref, computed, getCurrentInstance, watch } from "vue";
import { ref, computed, getCurrentInstance, watch, onMounted } from "vue";
import {update} from "@/api/productionManagement/productionProcess.js";
import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js";
import FormDialog from "@/components/Dialog/FormDialog.vue";
const props = defineProps({
  visible: {
@@ -84,14 +105,26 @@
const emit = defineEmits(['update:visible', 'completed']);
// å“åº”式数据(替代选项式的 data)
const processTypeOptions = [
  "机加工",
  "刮板冷芯制作",
  "管路组对",
  "罐体连接及调试",
  "测试打压",
  "其他",
];
const employeeOptions = ref([]);
const formState = ref({
  id: props.record.id,
  name: props.record.name,
  type: props.record.type,
  no: props.record.no,
  processType: props.record.processType || '',
  remark: props.record.remark,
  salaryQuota: props.record.salaryQuota,
  planPerson: props.record.planPerson || null,
  executor: props.record.executor || null,
  isQuality: props.record.isQuality,
  inbound: props.record.inbound,
  reportWork: props.record.reportWork,
@@ -106,16 +139,17 @@
  },
});
// ç›‘听 record å˜åŒ–,更新表单数据
watch(() => props.record, (newRecord) => {
  if (newRecord && isShow.value) {
    formState.value = {
      id: newRecord.id,
      name: newRecord.name || '',
      no: newRecord.no || '',
      type: newRecord.type,
      processType: newRecord.processType || '',
      remark: newRecord.remark || '',
      salaryQuota: newRecord.salaryQuota || '',
      planPerson: newRecord.planPerson || null,
      executor: newRecord.executor || null,
      isQuality: props.record.isQuality,
      inbound: newRecord.inbound,
      reportWork: newRecord.reportWork,
@@ -123,16 +157,17 @@
  }
}, { immediate: true, deep: true });
// ç›‘听弹窗打开,重新初始化表单数据
watch(() => props.visible, (visible) => {
  if (visible && props.record) {
    formState.value = {
      id: props.record.id,
      name: props.record.name || '',
      no: props.record.no || '',
      type: props.record.type,
      processType: props.record.processType || '',
      remark: props.record.remark || '',
      salaryQuota: props.record.salaryQuota || '',
      planPerson: props.record.planPerson || null,
      executor: props.record.executor || null,
      isQuality: props.record.isQuality,
      inbound: props.record.inbound,
      reportWork: props.record.reportWork,
@@ -150,9 +185,7 @@
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      update(formState.value).then(res => {
        // å…³é—­æ¨¡æ€æ¡†
        isShow.value = false;
        // å‘ŠçŸ¥çˆ¶ç»„件已完成
        emit('completed');
        proxy.$modal.msgSuccess("提交成功");
      })
@@ -160,6 +193,19 @@
  })
};
const loadEmployees = async () => {
  try {
    const res = await staffOnJobListPage({ current: -1, size: -1, staffState: 1 });
    employeeOptions.value = res.data?.records || [];
  } catch (error) {
    console.error("加载员工列表失败", error);
  }
};
onMounted(() => {
  loadEmployees();
});
defineExpose({
  closeModal,
  handleSubmit,
src/views/productionManagement/productionProcess/New.vue
@@ -1,19 +1,19 @@
<template>
  <div>
    <el-dialog
  <FormDialog
        v-model="isShow"
        title="新增工序"
        width="400"
        @close="closeModal"
    width="1000px"
    @confirm="handleSubmit"
    @cancel="closeModal"
    >
      <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
    <el-form label-width="180px" :model="formState" label-position="top" ref="formRef">
        <el-form-item
            label="工序名称:"
          label="部件:"
            prop="name"
            :rules="[
                {
                required: true,
                message: '请输入工序名称',
              message: '请输入部件',
              },
              {
                max: 100,
@@ -27,7 +27,7 @@
        </el-form-item>
        <el-form-item
            label="工序类型"
            prop="type"
          prop="processType"
            :rules="[
                {
                required: true,
@@ -35,15 +35,39 @@
              }
            ]"
        >
          <el-select v-model="formState.type" placeholder="请选择工序类型">
            <el-option label="计时" :value="0" />
            <el-option label="计件" :value="1" />
        <el-select v-model="formState.processType" placeholder="请选择工序类型" style="width: 100%">
          <el-option v-for="item in processTypeOptions"
                     :key="item"
                     :label="item"
                     :value="item" />
          </el-select>
        </el-form-item>
        <el-form-item label="工资定额" prop="salaryQuota">
          <el-input v-model="formState.salaryQuota" type="number" :step="0.001">
            <template #append>元</template>
          </el-input>
      <el-form-item label="计划工时(小时)" prop="salaryQuota">
        <el-input v-model="formState.salaryQuota" type="number" :step="0.5" />
      </el-form-item>
      <el-form-item label="计划人员" prop="planPerson">
        <el-select v-model="formState.planPerson"
                   placeholder="请选择计划人员"
                   clearable
                   filterable
                   style="width: 100%">
          <el-option v-for="item in employeeOptions"
                     :key="item.id"
                     :label="item.staffName"
                     :value="item.id" />
        </el-select>
      </el-form-item>
      <el-form-item label="计划执行人员" prop="executor">
        <el-select v-model="formState.executor"
                   placeholder="请选择计划执行人员"
                   clearable
                   filterable
                   style="width: 100%">
          <el-option v-for="item in employeeOptions"
                     :key="item.id"
                     :label="item.staffName"
                     :value="item.id" />
        </el-select>
        </el-form-item>
        <el-form-item label="是否质检" prop="isQuality">
          <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/>
@@ -58,19 +82,14 @@
          <el-input v-model="formState.remark" type="textarea" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确认</el-button>
          <el-button @click="closeModal">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
  </FormDialog>
</template>
<script setup>
import { ref, computed, getCurrentInstance } from "vue";
import { ref, computed, getCurrentInstance, onMounted } from "vue";
import {add} from "@/api/productionManagement/productionProcess.js";
import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js";
import FormDialog from "@/components/Dialog/FormDialog.vue";
const props = defineProps({
  visible: {
@@ -81,12 +100,25 @@
const emit = defineEmits(['update:visible', 'completed']);
// å“åº”式数据(替代选项式的 data)
const processTypeOptions = [
  "机加工",
  "刮板冷芯制作",
  "管路组对",
  "罐体连接及调试",
  "测试打压",
  "其他",
];
const employeeOptions = ref([]);
const formState = ref({
  name: '',
  type: undefined,
  no: '',
  processType: '',
  remark: '',
  salaryQuota:  '',
  planPerson: null,
  executor: null,
  isQuality: false,
  inbound: false,
  reportWork: false,
@@ -111,9 +143,7 @@
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      add(formState.value).then(res => {
        // å…³é—­æ¨¡æ€æ¡†
        isShow.value = false;
        // å‘ŠçŸ¥çˆ¶ç»„件已完成
        emit('completed');
        proxy.$modal.msgSuccess("提交成功");
      })
@@ -121,6 +151,19 @@
  })
};
const loadEmployees = async () => {
  try {
    const res = await staffOnJobListPage({ current: -1, size: -1, staffState: 1 });
    employeeOptions.value = res.data?.records || [];
  } catch (error) {
    console.error("加载员工列表失败", error);
  }
};
onMounted(() => {
  loadEmployees();
});
defineExpose({
  closeModal,
  handleSubmit,
src/views/productionManagement/productionProcess/index.vue
@@ -57,14 +57,8 @@
                        :type="process.isProduction ? 'warning' : 'info'">
                  {{ process.isProduction ? '生产' : '不生产' }}
                </el-tag>
                <el-tag v-if="process.type !== null && process.type !== undefined"
                        size="small"
                        :type="process.type == 1 ? 'primary' : 'success'"
                        style="margin-left: 8px">
                  {{ process.type == 0 ? '计时' : '计件' }}
                </el-tag>
              </div>
              <span class="param-count">工资定额: Â¥{{ process.salaryQuota || 0 }}</span>
              <span class="param-count">计划工时: {{ process.salaryQuota || 0 }}小时</span>
            </div>
          </div>
        </div>
@@ -101,28 +95,68 @@
      </div>
    </div>
    <!-- å·¥åºæ–°å¢ž/编辑对话框 -->
    <el-dialog v-model="processDialogVisible"
               :title="isProcessEdit ? '编辑工序' : '新增工序'"
               width="500px">
    <FormDialog v-model="processDialogVisible"
                :title="isProcessEdit ? '编辑部件' : '新增部件'"
                width="600"
                @confirm="handleProcessSubmit"
                @cancel="processDialogVisible = false">
      <el-form :model="processForm"
               :rules="processRules"
               ref="processFormRef"
               label-width="100px">
        <el-form-item label="工序编码"
                      prop="no">
          <el-input v-model="processForm.no"
                    placeholder="请输入工序编码" />
        </el-form-item>
        <el-form-item label="工序名称"
               label-width="120px">
        <el-form-item label="部件名称"
                      prop="name">
          <el-input v-model="processForm.name"
                    placeholder="请输入工序名称" />
                    placeholder="请输入部件名称" />
        </el-form-item>
        <el-form-item label="工资定额"
                <el-form-item label="部件编号"
                        prop="no">
                    <el-input v-model="processForm.no"
                        placeholder="请输入部件编号" />
                </el-form-item>
        <el-form-item label="部件类型"
                      prop="processType">
          <el-select v-model="processForm.processType"
                     placeholder="请选择部件类型"
                     style="width: 100%">
            <el-option v-for="item in processTypeOptions"
                       :key="item"
                       :label="item"
                       :value="item" />
          </el-select>
        </el-form-item>
        <el-form-item label="计划工时(小时)"
                      prop="salaryQuota">
          <el-input v-model="processForm.salaryQuota"
                    type="number"
                    :step="0.001" />
                    :step="0.5"
                    placeholder="请输入计划工时" />
        </el-form-item>
        <el-form-item label="计划人员"
                      prop="planPerson">
          <el-select v-model="processForm.planPerson"
                     placeholder="请选择计划人员"
                     clearable
                     filterable
                     style="width: 100%">
            <el-option v-for="item in employeeOptions"
                       :key="item.id"
                       :label="item.staffName"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="计划执行人员"
                      prop="executor">
          <el-select v-model="processForm.executor"
                     placeholder="请选择计划执行人员"
                     clearable
                     filterable
                     style="width: 100%">
            <el-option v-for="item in employeeOptions"
                       :key="item.id"
                       :label="item.staffName"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="是否质检"
                      prop="isQuality">
@@ -131,13 +165,6 @@
        <el-form-item label="是否生产"
                      prop="isProduction">
          <el-switch v-model="processForm.isProduction" />
        </el-form-item>
        <el-form-item label="计费类型"
                      prop="type">
          <el-radio-group v-model="processForm.type">
            <el-radio :label="0">计时</el-radio>
            <el-radio :label="1">计件</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="关联设备"
                      prop="deviceLedgerId">
@@ -160,16 +187,9 @@
                    placeholder="请输入工序描述" />
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary"
                     @click="handleProcessSubmit">确定</el-button>
          <el-button @click="processDialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
    </FormDialog>
    <!-- é€‰æ‹©å‚数对话框 -->
    <el-dialog v-model="paramDialogVisible"
    <FormDialog v-model="paramDialogVisible"
               title="选择参数"
               width="1000px">
      <div class="param-select-container">
@@ -259,7 +279,7 @@
          <el-button @click="paramDialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
    </FormDialog>
    <!-- ç¼–辑参数对话框 -->
    <el-dialog v-model="editParamDialogVisible"
               title="编辑参数"
@@ -308,6 +328,18 @@
  } from "@/api/productionManagement/productionProcess.js";
  import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
  import { getBaseParamList } from "@/api/basicData/parameterMaintenance.js";
  import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js";
  import FormDialog from "@/components/Dialog/FormDialog.vue";
  // éƒ¨ä»¶ç±»åž‹ä¸‹æ‹‰é€‰é¡¹ï¼ˆå†™æ­»ï¼‰
  const processTypeOptions = [
    "机加工",
    "刮板冷芯制作",
    "管路组对",
    "罐体连接及调试",
    "测试打压",
    "其他",
  ];
  // å·¥åºåˆ—表数据
  const processValueList = ref([]);
@@ -333,6 +365,9 @@
  // æ•°æ®å­—å…¸
  const dictTypes = ref([]);
  // å‘˜å·¥åˆ—表(计划人员、计划执行人员下拉用)
  const employeeOptions = ref([]);
  // å·¥åºå¯¹è¯æ¡†
  const processDialogVisible = ref(false);
  const isProcessEdit = ref(false);
@@ -346,19 +381,19 @@
    isProduction: false,
    remark: "",
    deviceLedgerId: null,
    type: 0,
    processType: "",
    planPerson: null,
    executor: null,
  });
  const processRules = {
    no: [{ required: true, message: "请输入工序编码", trigger: "blur" }],
    name: [{ required: true, message: "请输入工序名称", trigger: "blur" }],
    no: [{ required: true, message: "请输入部件编码", trigger: "blur" }],
    name: [{ required: true, message: "请输入部件名称", trigger: "blur" }],
    salaryQuota: [
      {
        required: false,
        message: "请输入工资定额",
        trigger: "blur",
        validator: (rule, value, callback) => {
          if (isNaN(value) || value < 0) {
            callback(new Error("工资定额必须是非负数字"));
          if (value !== null && value !== undefined && value !== "" && (isNaN(value) || Number(value) < 0)) {
            callback(new Error("计划工时必须是非负数字"));
          } else {
            callback();
          }
@@ -368,7 +403,7 @@
    deviceLedgerId: [
      { required: false, message: "请选择设备", trigger: "change" },
    ],
    type: [{ required: false, message: "请选择计费类型", trigger: "change" }],
    processType: [{ required: true, message: "请选择部件类型", trigger: "change" }],
  };
  // å‚数对话框
@@ -557,7 +592,9 @@
    processForm.isProduction = false;
    processForm.remark = "";
    processForm.deviceLedgerId = null;
    processForm.type = 0;
    processForm.processType = "";
    processForm.planPerson = null;
    processForm.executor = null;
    processDialogVisible.value = true;
  };
@@ -574,7 +611,9 @@
    const deviceId = Number(process.deviceLedgerId);
    const hasDevice = deviceOptions.value.some(item => item.id === deviceId);
    processForm.deviceLedgerId = deviceId && hasDevice ? deviceId : null;
    processForm.type = process.type;
    processForm.processType = process.processType || "";
    processForm.planPerson = process.planPerson || null;
    processForm.executor = process.executor || null;
    processDialogVisible.value = true;
  };
@@ -791,10 +830,20 @@
    });
  };
  const loadEmployees = async () => {
    try {
      const res = await staffOnJobListPage({ current: -1, size: -1, staffState: 1 });
      employeeOptions.value = res.data?.records || [];
    } catch (error) {
      console.error("加载员工列表失败", error);
    }
  };
  onMounted(() => {
    loadDeviceName();
    getProcessList();
    getDictTypes();
    loadEmployees();
  });
</script>
src/views/salesManagement/salesQuotation/ImportQuotationDetail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,105 @@
<script setup>
import {ref, computed, onMounted} from 'vue'
import {getQuotationRecordList} from "@/api/salesManagement/salesQuotationRecord.js";
const props = defineProps({
  showModal: {
    type: Boolean,
    required: true
  },
  quotationId: {
    type: Number,
    required: true
  }
})
const emit = defineEmits(['update:showModal'])
const dialogVisible = computed({
  get: () => props.showModal,
  set: (val) => emit('update:showModal', val)
})
const leftTableData = ref([])
const rightTableData = ref([])
const selectedLeftRow = ref(null)
const handleRowClick = (row) => {
  selectedLeftRow.value = row
  rightTableData.value = row.products || []
}
const tableRowClassName = ({row}) => {
  return selectedLeftRow.value === row ? 'selected-row' : ''
}
const fetchData = () => {
  getQuotationRecordList({quotationRecordId: props.quotationId}).then(res => {
    const data = res.data.records || []
    data.forEach(item => {
      leftTableData.value.push(JSON.parse(item?.info || '{}'))
    })
  })
}
const leftColumns = [
  {prop: 'customer', label: '客户名称', minWidth: '120'},
  {prop: 'salesperson', label: '业务员', minWidth: '120'},
  {prop: 'quotationDate', label: '报价日期', minWidth: '120'},
  {prop: 'validDate', label: '有效期至', minWidth: '120'},
  {prop: 'paymentMethod', label: '支付方式', minWidth: '120'},
  {prop: 'remark', label: '备注', minWidth: '120'}
]
const rightColumns = [
  {prop: 'product', label: '产品名称', minWidth: '120'},
  {prop: 'specification', label: '型号', minWidth: '120'},
  {prop: 'unit', label: '单位', minWidth: '100'},
  {prop: 'unitPrice', label: '单价', minWidth: '100'}
]
onMounted(() => {
  fetchData()
})
</script>
<template>
  <el-dialog v-model="dialogVisible" title="详情" width="1000px" :close-on-click-modal="false">
    <el-row :gutter="20">
      <el-col :span="12">
        <div class="table-title">报价单记录</div>
        <el-table :data="leftTableData" border height="400" stripe highlight-current-row
                  @row-click="handleRowClick" :row-class-name="tableRowClassName">
          <el-table-column v-for="col in leftColumns" :key="col.prop" :prop="col.prop" :label="col.label"
                           :min-width="col.minWidth" show-overflow-tooltip/>
        </el-table>
      </el-col>
      <el-col :span="12">
        <div class="table-title">报价单产品</div>
        <el-table :data="rightTableData" border height="400" stripe>
          <el-table-column v-for="col in rightColumns" :key="col.prop" :prop="col.prop" :label="col.label"
                           :min-width="col.minWidth" show-overflow-tooltip/>
        </el-table>
      </el-col>
    </el-row>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="dialogVisible = false">关闭</el-button>
      </div>
    </template>
  </el-dialog>
</template>
<style scoped>
.table-title {
  font-size: 15px;
  font-weight: bold;
  margin-bottom: 10px;
  color: #303133;
}
:deep(.selected-row) {
  background-color: #ecf5ff !important;
}
</style>
src/views/salesManagement/salesQuotation/index.vue
@@ -11,13 +11,16 @@
            @keyup.enter="handleSearch"
          >
            <template #prefix>
              <el-icon><Search /></el-icon>
              <el-icon>
                <Search/>
              </el-icon>
            </template>
          </el-input>
        </el-col>
        <el-col :span="8">
          <el-select v-model="searchForm.customerId" placeholder="请选择客户" clearable>
                        <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.id">
          <el-select v-model="searchForm.customer" placeholder="请选择客户" clearable>
            <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName"
                       :value="item.customerName">
                            {{
                                item.customerName + "——" + item.taxpayerIdentificationNumber
                            }}
@@ -35,9 +38,13 @@
        <el-col :span="8">
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
          <el-button style="float: right;" type="primary" @click="handleAdd">
            æ–°å¢žæŠ¥ä»·
          </el-button>
        </el-col>
      </el-row>
      <el-row :gutter="20" style="margin-bottom: 20px;">
        <el-col :span="24">
          <el-button type="primary" @click="handleAdd">新增报价</el-button>
          <el-button type="primary" @click="handleImport">导入销售报价</el-button>
          <el-button type="success" @click="handleShowImportLog">导入记录</el-button>
        </el-col>
      </el-row>
@@ -68,11 +75,14 @@
            Â¥{{ scope.row.totalAmount.toFixed(2) }}
          </template>
        </el-table-column>
        <el-table-column label="操作" width="200" fixed="right" align="center">
        <el-table-column label="操作" width="300" fixed="right" align="center">
          <template #default="scope">
            <el-button link type="primary" @click="handleEdit(scope.row)" :disabled="!['待审批','拒绝'].includes(scope.row.status)">编辑</el-button>
            <el-button link type="primary" @click="handleEdit(scope.row)"
                       >编辑
            </el-button>
            <el-button link type="primary" @click="handleView(scope.row)" style="color: #67C23A">查看</el-button>
            <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
            <el-button link type="info" @click="handleShowPriceHistory(scope.row)">降价历史</el-button>
          </template>
        </el-table-column>
      </el-table>
@@ -88,29 +98,38 @@
    </el-card>
    <!-- æ–°å¢ž/编辑对话框 -->
    <FormDialog v-model="dialogVisible" :title="dialogTitle" width="85%" :close-on-click-modal="false" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
    <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="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><Document /></el-icon>
                <el-icon class="card-icon">
                  <Document/>
                </el-icon>
              <span class="card-title">基本信息</span>
            </div>
          </template>
          <div class="form-content">
            <el-row :gutter="24">
              <el-col :span="12">
                <el-form-item label="客户名称" prop="customerId">
                  <el-select v-model="form.customerId" placeholder="请选择客户" style="width: 100%" clearable filterable>
                    <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.id"></el-option>
                  <el-form-item label="客户名称" prop="customer">
                    <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
                        }}
                      </el-option>
                  </el-select>
                </el-form-item>
              </el-col>
              <el-col :span="12">
                <el-form-item label="业务员" prop="salesperson">
                  <el-select v-model="form.salesperson" placeholder="请选择业务员" style="width: 100%" clearable filterable>
                    <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>
@@ -159,16 +178,21 @@
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><Box /></el-icon>
                <el-icon class="card-icon">
                  <Box/>
                </el-icon>
              <span class="card-title">产品信息</span>
              <el-button type="primary" size="small" @click="addProduct" class="header-btn">
                <el-icon><Plus /></el-icon>
                  <el-icon>
                    <Plus/>
                  </el-icon>
                æ·»åŠ äº§å“
              </el-button>
            </div>
          </template>
          <div class="form-content">
            <el-table :data="form.products" border style="width: 100%" class="product-table" v-if="form.products.length > 0">
              <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-form-item :prop="`products.${scope.$index}.productId`" class="product-table-form-item">
@@ -187,9 +211,9 @@
            </el-table-column>
            <el-table-column prop="specification" label="规格型号" width="200">
              <template #default="scope">
                <el-form-item :prop="`products.${scope.$index}.productModelId`" class="product-table-form-item">
                    <el-form-item :prop="`products.${scope.$index}.specificationId`" class="product-table-form-item">
                  <el-select
                    v-model="scope.row.productModelId"
                          v-model="scope.row.specificationId"
                    placeholder="请选择"
                    clearable
                    @change="getProductModel($event, scope.row)"
@@ -233,7 +257,9 @@
        <el-card class="form-card" shadow="hover">
          <template #header>
            <div class="card-header-wrapper">
              <el-icon class="card-icon"><EditPen /></el-icon>
                <el-icon class="card-icon">
                  <EditPen/>
                </el-icon>
              <span class="card-title">备注信息</span>
            </div>
          </template>
@@ -254,6 +280,73 @@
      </div>
    </FormDialog>
    <ImportDialog ref="importDialogRef"
                  v-model="importDialogVisible"
                  title="导入报价单"
                  :action="importAction"
                  :headers="importHeaders"
                  :auto-upload="false"
                  :on-success="handleImportSuccess"
                  :on-error="handleImportError"
                  @confirm="handleImportConfirm"
                  @download-template="handleDownloadTemplate"
                  @close="handleImportClose" />
    <!-- å¯¼å…¥è®°å½•对话框 -->
    <el-dialog v-model="importLogDialogVisible" title="导入记录" width="900px">
      <el-table :data="importLogList" border stripe v-loading="importLogLoading" height="400">
        <el-table-column align="center" label="序号" type="index" width="60"/>
        <el-table-column prop="batchNo" label="批次号" min-width="180"/>
        <el-table-column prop="fileName" label="文件名" min-width="160"/>
        <el-table-column prop="totalCount" label="总数" width="80" align="center"/>
        <el-table-column prop="successCount" label="成功" width="80" align="center"/>
        <el-table-column prop="failCount" label="失败" width="80" align="center"/>
        <el-table-column prop="status" label="状态" width="100" align="center">
          <template #default="{ row }">
            <el-tag :type="row.status === 'completed' ? 'success' : 'danger'" disable-transitions>
              {{ row.status === 'completed' ? '完成' : row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="createUserName" label="操作人" width="100"/>
        <el-table-column prop="createTime" label="导入时间" width="160"/>
      </el-table>
      <pagination
          v-if="importLogTotal > 0"
          :total="importLogTotal"
          layout="total, prev, pager, next"
          :page="importLogPage.current"
          :limit="importLogPage.size"
          @pagination="handleImportLogPageChange"
      />
    </el-dialog>
    <!-- é™ä»·åŽ†å²å¯¹è¯æ¡† -->
    <el-dialog v-model="priceHistoryDialogVisible" title="降价历史" width="900px">
      <el-table :data="priceHistoryList" border stripe v-loading="priceHistoryLoading" height="400">
        <el-table-column align="center" label="序号" type="index" width="60"/>
        <el-table-column prop="productName" label="产品名称" min-width="140"/>
        <el-table-column prop="specification" label="规格型号" min-width="120"/>
        <el-table-column prop="oldPrice" label="原价" width="100" align="center">
          <template #default="{ row }">Â¥{{ row.oldPrice?.toFixed(2) }}</template>
        </el-table-column>
        <el-table-column prop="newPrice" label="新价" width="100" align="center">
          <template #default="{ row }">Â¥{{ row.newPrice?.toFixed(2) }}</template>
        </el-table-column>
        <el-table-column prop="priceChange" label="变动" width="100" align="center">
          <template #default="{ row }">
            <span :style="{ color: row.priceChange < 0 ? '#67C23A' : '#F56C6C' }">
              {{ row.priceChange?.toFixed(2) }}
            </span>
          </template>
        </el-table-column>
        <el-table-column prop="changeReason" label="原因" width="100"/>
        <el-table-column prop="importBatch" label="导入批次" min-width="180"/>
        <el-table-column prop="importTime" label="导入时间" width="160"/>
        <el-table-column prop="createUserName" label="操作人" width="100"/>
      </el-table>
    </el-dialog>
    <!-- æŸ¥çœ‹è¯¦æƒ…对话框 -->
    <el-dialog v-model="viewDialogVisible" title="报价详情" width="800px">
      <el-descriptions :column="2" border>
@@ -267,7 +360,9 @@
<!--          <el-tag :type="getStatusType(currentQuotation.status)">{{ currentQuotation.status }}</el-tag>-->
<!--        </el-descriptions-item>-->
        <el-descriptions-item label="报价总额" :span="2">
          <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">Â¥{{ currentQuotation.totalAmount?.toFixed(2) }}</span>
          <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">Â¥{{
              currentQuotation.totalAmount?.toFixed(2)
            }}</span>
        </el-descriptions-item>
      </el-descriptions>
@@ -294,26 +389,46 @@
</template>
<script setup>
import { ref, reactive, computed, onMounted, markRaw, shallowRef } from 'vue'
import {ref, reactive, computed, onMounted, nextTick, getCurrentInstance} from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Document, Box, EditPen, Plus } 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'
import {modelList, productTreeList} from "@/api/basicData/product.js";
import {listCustomer} from "@/api/basicData/customer.js";
import ImportDialog from '@/components/Dialog/ImportDialog.vue'
import {
  getQuotationList,
  addQuotation,
  updateQuotation,
  deleteQuotation,
  downloadQuotationTemplate,
  getImportLogList,
  getPriceHistoryList
} from '@/api/salesManagement/salesQuotation.js'
import { userListNoPage } from "@/api/system/user.js";
import {customerList} from "@/api/salesManagement/salesLedger.js";
import {modelList, productTreeList} from "@/api/basicData/product.js";
import {getToken} from "@/utils/auth";
const {proxy} = getCurrentInstance();
// å“åº”式数据
const loading = ref(false)
const searchForm = reactive({
  quotationNo: '',
  customerId: '',
  customer: '',
  status: ''
})
const quotationList = ref([])
const userList = ref([])
const productOptions = ref([]);
const modelOptions  = ref([]);
const pagination = reactive({
@@ -323,11 +438,27 @@
})
const dialogVisible = ref(false)
const importDialogVisible = ref(false)
const importLogDialogVisible = ref(false)
const priceHistoryDialogVisible = ref(false)
const viewDialogVisible = ref(false)
const importDialogRef = ref(null)
const importAction = import.meta.env.VITE_APP_BASE_API + "/sales/quotation/import"
const importHeaders = ref({
  Authorization: `Bearer ${getToken()}`,
})
const importLogList = ref([])
const importLogLoading = ref(false)
const importLogTotal = ref(0)
const importLogPage = reactive({ current: 1, size: 10 })
const priceHistoryList = ref([])
const priceHistoryLoading = ref(false)
const currentQuotationForLog = ref(null)
const currentQuotationForPriceHistory = ref(null)
const dialogTitle = ref('新增报价')
const form = reactive({
  quotationNo: '',
  customerId: undefined,
  customer: '',
  salesperson: '',
  quotationDate: '',
@@ -354,7 +485,7 @@
const productRowRules = {
  productId: [{ required: true, message: '请选择产品名称', trigger: 'change' }],
  productModelId: [{ required: true, message: '请选择规格型号', trigger: 'change' }],
  specificationId: [{required: true, message: '请选择规格型号', trigger: 'change'}],
  unit: [{ required: true, message: '请填写单位', trigger: 'blur' }],
  unitPrice: [{ required: true, message: '请填写单价', trigger: 'change' }]
}
@@ -362,18 +493,109 @@
  const r = { ...baseRules }
  ;(form.products || []).forEach((_, i) => {
    r[`products.${i}.productId`] = productRowRules.productId
    r[`products.${i}.productModelId`] = productRowRules.productModelId
    r[`products.${i}.specificationId`] = productRowRules.specificationId
    r[`products.${i}.unit`] = productRowRules.unit
    r[`products.${i}.unitPrice`] = productRowRules.unitPrice
  })
  return r
})
const userList = ref([]);
const customerOption = ref([]);
const isEdit = ref(false)
const editId = ref(null)
const currentQuotation = ref({})
const formRef = ref()
// å¯¼å…¥æˆåŠŸ
const handleImportSuccess = (response) => {
  if (response.code === 200) {
    ElMessage.success("导入成功")
    importDialogVisible.value = false
    if (importDialogRef.value) {
      importDialogRef.value.clearFiles()
    }
    handleSearch()
  } else {
    ElMessage.error(response.msg || "导入失败")
  }
}
// å¯¼å…¥å¤±è´¥
const handleImportError = () => {
  ElMessage.error("导入失败,请检查文件格式是否正确")
}
// ç¡®è®¤å¯¼å…¥
const handleImportConfirm = () => {
  if (importDialogRef.value) {
    importDialogRef.value.submit()
  }
}
// ä¸‹è½½æ¨¡æ¿
const handleDownloadTemplate = () => {
  downloadQuotationTemplate().then(blob => {
    const url = window.URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = '报价单导入模板.xlsx'
    a.click()
    window.URL.revokeObjectURL(url)
  })
}
// å…³é—­å¯¼å…¥å¼¹çª—
const handleImportClose = () => {
  importDialogVisible.value = false
  if (importDialogRef.value) {
    importDialogRef.value.clearFiles()
  }
}
// å¯¼å…¥è®°å½•
const handleShowImportLog = () => {
  importLogPage.current = 1
  importLogDialogVisible.value = true
  fetchImportLogList()
}
const fetchImportLogList = () => {
  importLogLoading.value = true
  getImportLogList({ pageNum: importLogPage.current, pageSize: importLogPage.size }).then(res => {
    if (res.code === 200) {
      importLogList.value = res.data.records || []
      importLogTotal.value = res.data.total || 0
    }
  }).finally(() => {
    importLogLoading.value = false
  })
}
const handleImportLogPageChange = (val) => {
  importLogPage.current = val.page
  importLogPage.size = val.limit
  fetchImportLogList()
}
// é™ä»·åŽ†å²
const handleShowPriceHistory = (row) => {
  currentQuotationForPriceHistory.value = row
  priceHistoryDialogVisible.value = true
  priceHistoryList.value = []
  fetchPriceHistoryList(row)
}
const fetchPriceHistoryList = (row) => {
  priceHistoryLoading.value = true
  getPriceHistoryList({ quotationProductId: row.id }).then(res => {
    priceHistoryList.value = res.data
  }).finally(() => {
    priceHistoryLoading.value = false
  })
}
const handlePriceHistoryPageChange = () => {}
// è®¡ç®—属性
const filteredList = computed(() => {
@@ -401,19 +623,36 @@
  handleSearch()
}
// å¯¼å…¥æ–‡ä»¶
const handleImport = () => {
  importDialogVisible.value = true
  nextTick(() => {
    if (importDialogRef.value) {
      importDialogRef.value.clearFiles()
    }
  })
}
const handleAdd = async () => {
  dialogTitle.value = '新增报价'
  isEdit.value = false
  resetForm()
  dialogVisible.value = true
  let userLists = await userListNoPage();
  // åªå¤åˆ¶éœ€è¦çš„字段,避免将组件引用放入响应式对象
  userList.value = (userLists.data || []).map(item => ({
    userId: item.userId,
    nickName: item.nickName || '',
    userName: item.userName || ''
  }));
  getProductOptions();
  fetchCustomerOptions()
}
const fetchCustomerOptions = () => {
  if (customerOption.value.length > 0) return
  listCustomer({current: -1,size:-1, type: 0}).then((res) => {
    customerOption.value = res.data.records;
  customerList().then((res) => {
    // åªå¤åˆ¶éœ€è¦çš„字段,避免将组件引用放入响应式对象
    customerOption.value = (Array.isArray(res) ? res : []).map(item => ({
      id: item.id,
      customerName: item.customerName || '',
      taxpayerIdentificationNumber: item.taxpayerIdentificationNumber || ''
    }))
  });
}
const getProductOptions = () => {
@@ -423,6 +662,7 @@
        return productOptions.value
    });
};
function convertIdToValue(data) {
    return data.map((item) => {
        const { id, children, ...rest } = item;
@@ -437,6 +677,7 @@
        return newItem;
    });
}
// æ ¹æ®åç§°åæŸ¥èŠ‚ç‚¹ id,便于仅存名称时的反显
function findNodeIdByLabel(nodes, label) {
    if (!label) return null;
@@ -450,6 +691,7 @@
    }
    return null;
}
const getModels = (value, row) => {
    if (!row) return;
    // å¦‚果清空选择,则清空相关字段
@@ -457,7 +699,7 @@
        row.productId = '';
        row.product = '';
        row.modelOptions = [];
        row.productModelId = '';
    row.specificationId = '';
        row.specification = '';
        row.unit = '';
        return;
@@ -478,13 +720,13 @@
    if (!row) return;
    // å¦‚果清空选择,则清空相关字段
    if (!value) {
        row.productModelId = '';
    row.specificationId = '';
        row.specification = '';
        row.unit = '';
        return;
    }
    // æ›´æ–° productModelId(v-model å·²ç»è‡ªåŠ¨æ›´æ–°ï¼Œè¿™é‡Œç¡®ä¿ä¸€è‡´æ€§ï¼‰
    row.productModelId = value;
  // æ›´æ–° specificationId(v-model å·²ç»è‡ªåŠ¨æ›´æ–°ï¼Œè¿™é‡Œç¡®ä¿ä¸€è‡´æ€§ï¼‰
  row.specificationId = value;
    const modelOptions = row.modelOptions || [];
    const index = modelOptions.findIndex((item) => item.id === value);
    if (index !== -1) {
@@ -523,7 +765,7 @@
    products: row.products ? row.products.map(product => ({
      productId: product.productId || '',
      product: product.product || product.productName || '',
      productModelId: product.productModelId || '',
      specificationId: product.specificationId || '',
      specification: product.specification || '',
      quantity: product.quantity || 0,
      unit: product.unit || '',
@@ -542,12 +784,10 @@
  form.id = row.id || form.id || null
  // å…ˆåŠ è½½äº§å“æ ‘æ•°æ®ï¼Œå¦åˆ™ el-tree-select æ— æ³•反显产品名称
  await getProductOptions()
  await fetchCustomerOptions()
  // åªå¤åˆ¶éœ€è¦çš„字段,避免将组件引用放入响应式对象
  form.quotationNo = row.quotationNo || ''
  form.customer = row.customer || ''
  form.customerId = row.customerId || undefined
  form.salesperson = row.salesperson || ''
  form.quotationDate = row.quotationDate || ''
  form.validDate = row.validDate || ''
@@ -563,18 +803,18 @@
    // å¦‚果有产品ID,加载对应的规格型号列表
    let modelOptions = [];
    let resolvedProductModelId = product.productModelId || '';
    let resolvedSpecificationId = product.specificationId || '';
    if (resolvedProductId) {
      try {
        const res = await modelList({ id: resolvedProductId });
        modelOptions = res || [];
        // å¦‚果返回的数据没有 productModelId,但有 specification åç§°ï¼Œæ ¹æ®åç§°æŸ¥æ‰¾ ID
        if (!resolvedProductModelId && product.specification) {
        // å¦‚果返回的数据没有 specificationId,但有 specification åç§°ï¼Œæ ¹æ®åç§°æŸ¥æ‰¾ ID
        if (!resolvedSpecificationId && product.specification) {
          const foundModel = modelOptions.find(item => item.model === product.specification);
          if (foundModel) {
            resolvedProductModelId = foundModel.id;
            resolvedSpecificationId = foundModel.id;
          }
        }
      } catch (error) {
@@ -585,7 +825,7 @@
    return {
      productId: resolvedProductId,
      product: productName,
      productModelId: resolvedProductModelId,
      specificationId: resolvedSpecificationId,
      specification: product.specification || '',
      quantity: product.quantity || 0,
      unit: product.unit || '',
@@ -600,6 +840,14 @@
  form.discountRate = row.discountRate || 0
  form.discountAmount = row.discountAmount || 0
  form.totalAmount = row.totalAmount || 0
  // åŠ è½½ç”¨æˆ·åˆ—è¡¨
  let userLists = await userListNoPage();
  userList.value = (userLists.data || []).map(item => ({
    userId: item.userId,
    nickName: item.nickName || '',
    userName: item.userName || ''
  }));
  dialogVisible.value = true
}
@@ -649,7 +897,8 @@
    productId: '',
    product: '',
    productName: '',
    productModelId: '',
    specificationId: '',
    specification: '',
    quantity: 1,
    unit: '',
    unitPrice: 0,
@@ -678,6 +927,10 @@
  form.totalAmount = form.subtotal + form.freight + form.otherFee - form.discountAmount
}
const handleCustomerChange = () => {
  // å¯ä»¥æ ¹æ®å®¢æˆ·ä¿¡æ¯è‡ªåŠ¨å¡«å……ä¸€äº›é»˜è®¤å€¼
}
const handleSubmit = () => {
  formRef.value.validate((valid) => {
    if (valid) {
@@ -692,7 +945,6 @@
        return sum + price
      }, 0)
      form.customer = customerOption.value.find(item => item.id === form.customerId)?.customerName || ''
      if (isEdit.value) {
        // ç¼–辑
        const index = quotationList.value.findIndex(item => item.id === editId.value)
@@ -721,6 +973,10 @@
  })
}
const downloadImportTemplate = () => {
  proxy.download("/sales/quotation/downloadTemplate", {}, "报价单导入模板.xlsx");
}
const handleCurrentChange = (val) => {
  pagination.currentPage = val.page
  pagination.pageSize = val.limit
@@ -742,19 +998,16 @@
        id: item.id,
        quotationNo: item.quotationNo || '',
        customer: item.customer || '',
        customerId: item.customerId || undefined,
        salesperson: item.salesperson || '',
        quotationDate: item.quotationDate || '',
        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 || '',
          product: product.product || product.productName || '',
          productModelId: product.productModelId || '',
          specificationId: product.specificationId || '',
          specification: product.specification || '',
          quantity: product.quantity || 0,
          unit: product.unit || '',
@@ -771,25 +1024,18 @@
      pagination.total = res.data.total
    }
  })
    // customerList().then((res) => {
    //     customerOption.value = res;
    // });
}
const getUserList = async () => {
  try {
    const res = await userListNoPage()
    userList.value = Array.isArray(res?.data) ? res.data : []
  } catch (error) {
    userList.value = []
    ElMessage.error('加载业务员列表失败')
  }
  customerList().then((res) => {
    // åªå¤åˆ¶éœ€è¦çš„字段,避免将组件引用放入响应式对象
    customerOption.value = (Array.isArray(res) ? res : []).map(item => ({
      id: item.id,
      customerName: item.customerName || '',
      taxpayerIdentificationNumber: item.taxpayerIdentificationNumber || ''
    }))
  });
}
onMounted(()=>{
  getUserList()
  handleSearch()
  fetchCustomerOptions()
})
</script>
@@ -872,13 +1118,80 @@
.product-table-form-item {
  margin-bottom: 0;
  :deep(.el-form-item__content) {
    margin-left: 0 !important;
  }
  :deep(.el-form-item__label) {
    width: auto;
    min-width: auto;
  }
}
.approver-nodes-container {
  display: flex;
  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 {
@@ -907,4 +1220,14 @@
  text-align: right;
}
// å“åº”式优化
@media (max-width: 1200px) {
  .approver-nodes-container {
    gap: 16px;
  }
  .approver-node-item {
    min-width: 160px;
  }
}
</style>